Prove embedded surface slots across Eve runtimes
This commit is contained in:
parent
614844ee2b
commit
dabb3e692f
8 changed files with 255 additions and 85 deletions
|
|
@ -130,6 +130,46 @@ static NSData *EVEMediaObservation(NSString *observationId,
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static NSArray<NSURL *> *EVEConfiguredURLs(NSString *key) {
|
||||||
|
NSUserDefaults *defaults = NSUserDefaults.standardUserDefaults;
|
||||||
|
id configured = [defaults objectForKey:key];
|
||||||
|
NSMutableArray<NSString *> *values = [NSMutableArray array];
|
||||||
|
|
||||||
|
if ([configured isKindOfClass:NSArray.class]) {
|
||||||
|
for (id item in (NSArray *)configured) {
|
||||||
|
if ([item isKindOfClass:NSString.class]) {
|
||||||
|
[values addObject:item];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if ([configured isKindOfClass:NSString.class]) {
|
||||||
|
NSCharacterSet *separators = [NSCharacterSet characterSetWithCharactersInString:@", \n\t"];
|
||||||
|
for (NSString *part in [(NSString *)configured componentsSeparatedByCharactersInSet:separators]) {
|
||||||
|
if (part.length > 0) {
|
||||||
|
[values addObject:part];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
NSString *envValue = NSProcessInfo.processInfo.environment[key];
|
||||||
|
if ([envValue isKindOfClass:NSString.class] && envValue.length > 0) {
|
||||||
|
NSCharacterSet *separators = [NSCharacterSet characterSetWithCharactersInString:@", \n\t"];
|
||||||
|
for (NSString *part in [envValue componentsSeparatedByCharactersInSet:separators]) {
|
||||||
|
if (part.length > 0) {
|
||||||
|
[values addObject:part];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
NSMutableArray<NSURL *> *urls = [NSMutableArray array];
|
||||||
|
for (NSString *value in values) {
|
||||||
|
NSURL *url = [NSURL URLWithString:value];
|
||||||
|
if (url) {
|
||||||
|
[urls addObject:url];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return urls;
|
||||||
|
}
|
||||||
|
|
||||||
@interface EVEViewController () <EVEFrameStreamClientDelegate, EVEDashboardClientDelegate, AVCaptureVideoDataOutputSampleBufferDelegate, UIGestureRecognizerDelegate>
|
@interface EVEViewController () <EVEFrameStreamClientDelegate, EVEDashboardClientDelegate, AVCaptureVideoDataOutputSampleBufferDelegate, UIGestureRecognizerDelegate>
|
||||||
|
|
||||||
@property(nonatomic, strong) EVEGLView *glView;
|
@property(nonatomic, strong) EVEGLView *glView;
|
||||||
|
|
@ -240,39 +280,23 @@ static NSData *EVEMediaObservation(NSString *observationId,
|
||||||
self.streamScale = 2.0;
|
self.streamScale = 2.0;
|
||||||
self.streamStatus = @"stream idle";
|
self.streamStatus = @"stream idle";
|
||||||
self.streamCodec = @"jpeg";
|
self.streamCodec = @"jpeg";
|
||||||
self.dialogueLine = @"awaiting Mimir";
|
self.dialogueLine = @"awaiting configured stream";
|
||||||
self.dashboardStatus = @"dashboard idle";
|
self.dashboardStatus = @"dashboard idle";
|
||||||
self.dashboardProviderId = @"";
|
self.dashboardProviderId = @"";
|
||||||
self.activeSurfaceFontScale = 1.0;
|
self.activeSurfaceFontScale = 1.0;
|
||||||
self.nodeViews = [NSMutableDictionary dictionary];
|
self.nodeViews = [NSMutableDictionary dictionary];
|
||||||
self.dashboardNodes = [NSMutableDictionary dictionary];
|
self.dashboardNodes = [NSMutableDictionary dictionary];
|
||||||
self.avatarCache = [NSMutableDictionary dictionary];
|
self.avatarCache = [NSMutableDictionary dictionary];
|
||||||
NSArray<NSURL *> *streamURLs = @[
|
NSArray<NSURL *> *streamURLs = EVEConfiguredURLs(@"EVE_STREAM_URLS");
|
||||||
[NSURL URLWithString:@"ws://127.0.0.1:8792/stream"],
|
|
||||||
[NSURL URLWithString:@"ws://192.168.1.66:8792/stream"],
|
|
||||||
];
|
|
||||||
self.streamClient = [[EVEFrameStreamClient alloc] initWithURLs:streamURLs delegate:self];
|
self.streamClient = [[EVEFrameStreamClient alloc] initWithURLs:streamURLs delegate:self];
|
||||||
[self.streamClient connect];
|
[self.streamClient connect];
|
||||||
|
|
||||||
NSArray<NSURL *> *dashboardURLs = @[
|
NSArray<NSURL *> *dashboardURLs = EVEConfiguredURLs(@"EVE_DASHBOARD_URLS");
|
||||||
[NSURL URLWithString:@"ws://127.0.0.1:8797/eve/deck"],
|
|
||||||
[NSURL URLWithString:@"ws://192.168.1.66:8797/eve/deck"],
|
|
||||||
[NSURL URLWithString:@"ws://127.0.0.1:8795/eve/deck"],
|
|
||||||
[NSURL URLWithString:@"ws://192.168.1.66:8795/eve/deck"],
|
|
||||||
[NSURL URLWithString:@"ws://127.0.0.1:8795/eve/dashboard"],
|
|
||||||
[NSURL URLWithString:@"ws://192.168.1.66:8795/eve/dashboard"],
|
|
||||||
];
|
|
||||||
self.dashboardClient = [[EVEDashboardClient alloc] initWithURLs:dashboardURLs delegate:self];
|
self.dashboardClient = [[EVEDashboardClient alloc] initWithURLs:dashboardURLs delegate:self];
|
||||||
[self.dashboardClient connect];
|
[self.dashboardClient connect];
|
||||||
|
|
||||||
NSArray<NSURL *> *cameraURLs = @[
|
NSArray<NSURL *> *cameraURLs = EVEConfiguredURLs(@"EVE_CAMERA_URLS");
|
||||||
[NSURL URLWithString:@"ws://127.0.0.1:8793/eve/camera"],
|
NSArray<NSURL *> *micURLs = EVEConfiguredURLs(@"EVE_MIC_URLS");
|
||||||
[NSURL URLWithString:@"ws://192.168.1.66:8793/eve/camera"],
|
|
||||||
];
|
|
||||||
NSArray<NSURL *> *micURLs = @[
|
|
||||||
[NSURL URLWithString:@"ws://127.0.0.1:8794/eve/mic"],
|
|
||||||
[NSURL URLWithString:@"ws://192.168.1.66:8794/eve/mic"],
|
|
||||||
];
|
|
||||||
self.cameraUplink = [[EVESensorUplinkClient alloc] initWithURLs:cameraURLs label:@"camera"];
|
self.cameraUplink = [[EVESensorUplinkClient alloc] initWithURLs:cameraURLs label:@"camera"];
|
||||||
self.micUplink = [[EVESensorUplinkClient alloc] initWithURLs:micURLs label:@"mic"];
|
self.micUplink = [[EVESensorUplinkClient alloc] initWithURLs:micURLs label:@"mic"];
|
||||||
[self.cameraUplink connect];
|
[self.cameraUplink connect];
|
||||||
|
|
@ -1257,6 +1281,27 @@ static NSData *EVEMediaObservation(NSString *observationId,
|
||||||
return [self renderSurfaceRail:element props:props children:children depth:depth];
|
return [self renderSurfaceRail:element props:props children:children depth:depth];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ([kind isEqualToString:@"surface.slot"]) {
|
||||||
|
NSArray *slots = [element[@"embeddedDocuments"] isKindOfClass:NSArray.class] ? element[@"embeddedDocuments"] : @[];
|
||||||
|
NSDictionary *slot = slots.count > 0 && [slots[0] isKindOfClass:NSDictionary.class] ? slots[0] : @{};
|
||||||
|
NSString *slotId = [props[@"slotId"] isKindOfClass:NSString.class] ? props[@"slotId"] : ([slot[@"slotId"] isKindOfClass:NSString.class] ? slot[@"slotId"] : @"");
|
||||||
|
NSString *documentId = [props[@"documentId"] isKindOfClass:NSString.class] ? props[@"documentId"] : ([slot[@"documentId"] isKindOfClass:NSString.class] ? slot[@"documentId"] : @"");
|
||||||
|
NSString *schemaId = [props[@"schemaId"] isKindOfClass:NSString.class] ? props[@"schemaId"] : ([slot[@"schemaId"] isKindOfClass:NSString.class] ? slot[@"schemaId"] : @"");
|
||||||
|
NSString *presentationKind = [props[@"presentationKind"] isKindOfClass:NSString.class] ? props[@"presentationKind"] : ([slot[@"presentationKind"] isKindOfClass:NSString.class] ? slot[@"presentationKind"] : @"");
|
||||||
|
NSString *title = presentationKind.length > 0 ? presentationKind : (slotId.length > 0 ? slotId : @"embedded surface");
|
||||||
|
NSString *detail = [@[documentId ?: @"", schemaId ?: @""] componentsJoinedByString:@"\n"];
|
||||||
|
UILabel *label = [self surfaceLabelWithText:[NSString stringWithFormat:@"%@\n%@", title.uppercaseString, detail]
|
||||||
|
size:11.0
|
||||||
|
weight:UIFontWeightMedium
|
||||||
|
color:[self fensalirPrimaryTextColor]];
|
||||||
|
label.accessibilityIdentifier = [NSString stringWithFormat:@"embeddedDocuments:%@:%@:%@:%@", slotId ?: @"", documentId ?: @"", schemaId ?: @"", presentationKind ?: @""];
|
||||||
|
label.layer.borderWidth = 1.0;
|
||||||
|
label.layer.borderColor = [self fensalirAccentSoftColor].CGColor;
|
||||||
|
label.layer.cornerRadius = 5.0;
|
||||||
|
label.backgroundColor = [UIColor colorWithRed:0.014 green:0.050 blue:0.052 alpha:0.92];
|
||||||
|
return label;
|
||||||
|
}
|
||||||
|
|
||||||
if ([kind isEqualToString:@"avatar"]) {
|
if ([kind isEqualToString:@"avatar"]) {
|
||||||
return [self renderSurfaceAvatar:element props:props];
|
return [self renderSurfaceAvatar:element props:props];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -51,8 +51,6 @@ import org.gamecult.cultmesh.eve.EveDashboardUiElement
|
||||||
import org.gamecult.cultmesh.eve.EveMediaObservationDocument
|
import org.gamecult.cultmesh.eve.EveMediaObservationDocument
|
||||||
import org.gamecult.cultmesh.eve.EveSensorObservationDocument
|
import org.gamecult.cultmesh.eve.EveSensorObservationDocument
|
||||||
import java.net.URI
|
import java.net.URI
|
||||||
import java.net.HttpURLConnection
|
|
||||||
import java.net.URL
|
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
|
@ -61,13 +59,18 @@ import java.util.concurrent.Executors
|
||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
|
|
||||||
class MainActivity : Activity(), SensorEventListener {
|
class MainActivity : Activity(), SensorEventListener {
|
||||||
|
companion object {
|
||||||
|
const val EXTRA_DASHBOARD_URI = "org.gamecult.eve.DASHBOARD_URI"
|
||||||
|
const val EXTRA_SENSOR_URI = "org.gamecult.eve.SENSOR_URI"
|
||||||
|
}
|
||||||
|
|
||||||
private val mesh = CultMeshNode()
|
private val mesh = CultMeshNode()
|
||||||
private val main = Handler(Looper.getMainLooper())
|
private val main = Handler(Looper.getMainLooper())
|
||||||
private val workers = Executors.newCachedThreadPool()
|
private val workers = Executors.newCachedThreadPool()
|
||||||
private val deviceId = "periwinkle"
|
private val deviceId = "periwinkle"
|
||||||
private val clientId = "eve-android-periwinkle"
|
private val clientId = "eve-android-periwinkle"
|
||||||
private val dashboardUri = URI.create("ws://192.168.1.66:8795/eve/deck/cultmesh")
|
private var dashboardUri: URI? = null
|
||||||
private val sensorUri = URI.create("ws://192.168.1.66:8796/eve/periwinkle")
|
private var sensorUri: URI? = null
|
||||||
|
|
||||||
private var sensorManager: SensorManager? = null
|
private var sensorManager: SensorManager? = null
|
||||||
private var dashboardSocket: CultNetWebSocketClient? = null
|
private var dashboardSocket: CultNetWebSocketClient? = null
|
||||||
|
|
@ -102,6 +105,8 @@ class MainActivity : Activity(), SensorEventListener {
|
||||||
setContentView(buildParityFixtureUi())
|
setContentView(buildParityFixtureUi())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
dashboardUri = resolveDashboardUri(intent.getStringExtra(EXTRA_DASHBOARD_URI))
|
||||||
|
sensorUri = resolveConfiguredUri(intent.getStringExtra(EXTRA_SENSOR_URI))
|
||||||
sensorManager = getSystemService(SENSOR_SERVICE) as? SensorManager
|
sensorManager = getSystemService(SENSOR_SERVICE) as? SensorManager
|
||||||
setContentView(buildUi())
|
setContentView(buildUi())
|
||||||
startSensors()
|
startSensors()
|
||||||
|
|
@ -197,6 +202,7 @@ class MainActivity : Activity(), SensorEventListener {
|
||||||
addView(renderCultUiNode(child, values, tokens), stageParams(child))
|
addView(renderCultUiNode(child, values, tokens), stageParams(child))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
"surface.slot" -> embeddedSurfaceSlot(node, props, tokens)
|
||||||
"control.button" -> label(props.optString("label", ""), 14f, tokenColor(tokens, "colorText", 0xfff6f1e2.toInt()), true).apply {
|
"control.button" -> label(props.optString("label", ""), 14f, tokenColor(tokens, "colorText", 0xfff6f1e2.toInt()), true).apply {
|
||||||
gravity = Gravity.CENTER
|
gravity = Gravity.CENTER
|
||||||
setPadding(dp(14), dp(12), dp(14), dp(12))
|
setPadding(dp(14), dp(12), dp(14), dp(12))
|
||||||
|
|
@ -322,6 +328,34 @@ class MainActivity : Activity(), SensorEventListener {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun embeddedSurfaceSlot(node: JSONObject, props: JSONObject, tokens: JSONObject): View {
|
||||||
|
val slots = node.optJSONArray("embeddedDocuments")
|
||||||
|
val slot = slots?.optJSONObject(0)
|
||||||
|
val slotId = props.optString("slotId", slot?.optString("slotId", "") ?: "")
|
||||||
|
val documentId = props.optString("documentId", slot?.optString("documentId", "") ?: "")
|
||||||
|
val schemaId = props.optString("schemaId", slot?.optString("schemaId", "") ?: "")
|
||||||
|
val presentationKind = props.optString("presentationKind", slot?.optString("presentationKind", "") ?: "")
|
||||||
|
val title = presentationKind.ifBlank { slotId.ifBlank { "embedded surface" } }
|
||||||
|
val detail = listOf(documentId, schemaId)
|
||||||
|
.filter { it.isNotBlank() }
|
||||||
|
.joinToString("\n")
|
||||||
|
|
||||||
|
return LinearLayout(this).apply {
|
||||||
|
orientation = LinearLayout.VERTICAL
|
||||||
|
setPadding(dp(12), dp(10), dp(12), dp(10))
|
||||||
|
background = android.graphics.drawable.GradientDrawable().apply {
|
||||||
|
setColor(tokenColor(tokens, "colorPanel", Color.rgb(7, 25, 24)))
|
||||||
|
setStroke(dp(1), tokenColor(tokens, "colorAccent", 0xffff8a2a.toInt()))
|
||||||
|
cornerRadius = dp(6).toFloat()
|
||||||
|
}
|
||||||
|
addView(label(title, 12f, tokenColor(tokens, "colorAccent", 0xffff8a2a.toInt()), true))
|
||||||
|
if (detail.isNotBlank()) {
|
||||||
|
addView(label(detail, 11f, tokenColor(tokens, "colorMuted", 0xffd3bb7f.toInt()), false))
|
||||||
|
}
|
||||||
|
contentDescription = "embeddedDocuments:$slotId:$documentId:$schemaId:$presentationKind"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun stageParams(node: JSONObject): FrameLayout.LayoutParams {
|
private fun stageParams(node: JSONObject): FrameLayout.LayoutParams {
|
||||||
val kind = node.optString("kind", "")
|
val kind = node.optString("kind", "")
|
||||||
val props = node.optJSONObject("props") ?: JSONObject()
|
val props = node.optJSONObject("props") ?: JSONObject()
|
||||||
|
|
@ -430,9 +464,9 @@ class MainActivity : Activity(), SensorEventListener {
|
||||||
scroll.addView(root, ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT))
|
scroll.addView(root, ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT))
|
||||||
root.addView(label("Eve / Periwinkle", 18f, 0xff8efcff.toInt(), true))
|
root.addView(label("Eve / Periwinkle", 18f, 0xff8efcff.toInt(), true))
|
||||||
root.addView(label("CultMesh dashboard and sensor edge", 11f, 0xffb7c7c7.toInt(), false))
|
root.addView(label("CultMesh dashboard and sensor edge", 11f, 0xffb7c7c7.toInt(), false))
|
||||||
brokerText = card("CultMesh broker\nconnecting $dashboardUri")
|
brokerText = card("CultMesh broker\n${dashboardUriText()}")
|
||||||
selectedText = card("selection\nwaiting for dashboard state")
|
selectedText = card("selection\nwaiting for dashboard state")
|
||||||
sensorText = card("CultMesh sensors\nconnecting $sensorUri")
|
sensorText = card("CultMesh sensors\n${sensorUriText()}")
|
||||||
touchText = card("touch surface\nwaiting for operator input")
|
touchText = card("touch surface\nwaiting for operator input")
|
||||||
mediaText = card("media sensors\nwaiting for camera/mic permissions")
|
mediaText = card("media sensors\nwaiting for camera/mic permissions")
|
||||||
providerPicker = Spinner(this).apply {
|
providerPicker = Spinner(this).apply {
|
||||||
|
|
@ -471,7 +505,6 @@ class MainActivity : Activity(), SensorEventListener {
|
||||||
root.addView(mediaText)
|
root.addView(mediaText)
|
||||||
root.addView(touchText)
|
root.addView(touchText)
|
||||||
root.addView(card("contract\nPeriwinkle consumes mimir.eve_dashboard_state.v1, sends mimir.eve_dashboard_command.v1, and publishes mimir.eve_sensor_observation.v1 plus mimir.eve_media_observation.v1. Mimir accepts meaning; Android renders and observes."))
|
root.addView(card("contract\nPeriwinkle consumes mimir.eve_dashboard_state.v1, sends mimir.eve_dashboard_command.v1, and publishes mimir.eve_sensor_observation.v1 plus mimir.eve_media_observation.v1. Mimir accepts meaning; Android renders and observes."))
|
||||||
refreshProviderCatalog()
|
|
||||||
return scroll
|
return scroll
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -496,11 +529,16 @@ class MainActivity : Activity(), SensorEventListener {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun connectDashboard() {
|
private fun connectDashboard() {
|
||||||
|
val uri = dashboardUri
|
||||||
|
if (uri == null) {
|
||||||
|
postBroker("CultMesh broker\nno dashboard endpoint configured\nwaiting for Odin/CultMesh discovery")
|
||||||
|
return
|
||||||
|
}
|
||||||
workers.execute {
|
workers.execute {
|
||||||
while (!Thread.currentThread().isInterrupted) {
|
while (!Thread.currentThread().isInterrupted) {
|
||||||
try {
|
try {
|
||||||
dashboardSocket = mesh.connect(dashboardUri)
|
dashboardSocket = mesh.connect(uri)
|
||||||
postBroker("CultMesh broker\nconnected $dashboardUri\ndocument=mimir.eve_dashboard_state.v1")
|
postBroker("CultMesh broker\nconnected $uri\ndocument=mimir.eve_dashboard_state.v1")
|
||||||
while (!Thread.currentThread().isInterrupted) {
|
while (!Thread.currentThread().isInterrupted) {
|
||||||
val frame: CultNetFrame = dashboardSocket!!.readFrame()
|
val frame: CultNetFrame = dashboardSocket!!.readFrame()
|
||||||
if (frame.opcode == 0x8) error("dashboard closed")
|
if (frame.opcode == 0x8) error("dashboard closed")
|
||||||
|
|
@ -511,22 +549,45 @@ class MainActivity : Activity(), SensorEventListener {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (ex: Exception) {
|
} catch (ex: Exception) {
|
||||||
postBroker("CultMesh broker\nwaiting for Mimir\n${ex.javaClass.simpleName}: ${ex.message}")
|
postBroker("CultMesh broker\nwaiting for configured CultMesh dashboard\n${ex.javaClass.simpleName}: ${ex.message}")
|
||||||
Thread.sleep(2000)
|
Thread.sleep(2000)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun resolveDashboardUri(value: String?): URI? = resolveConfiguredUri(value)
|
||||||
|
|
||||||
|
private fun resolveConfiguredUri(value: String?): URI? {
|
||||||
|
val text = value?.trim().orEmpty()
|
||||||
|
if (text.isBlank()) return null
|
||||||
|
return try {
|
||||||
|
URI.create(text)
|
||||||
|
} catch (_: IllegalArgumentException) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun dashboardUriText(): String =
|
||||||
|
dashboardUri?.let { "connecting $it" } ?: "no dashboard endpoint configured"
|
||||||
|
|
||||||
|
private fun sensorUriText(): String =
|
||||||
|
sensorUri?.let { "connecting $it" } ?: "no sensor endpoint configured"
|
||||||
|
|
||||||
private fun connectSensorUplink() {
|
private fun connectSensorUplink() {
|
||||||
|
val uri = sensorUri
|
||||||
|
if (uri == null) {
|
||||||
|
postSensor("CultMesh sensors\nno sensor endpoint configured\nwaiting for Odin/CultMesh discovery")
|
||||||
|
return
|
||||||
|
}
|
||||||
workers.execute {
|
workers.execute {
|
||||||
while (!Thread.currentThread().isInterrupted) {
|
while (!Thread.currentThread().isInterrupted) {
|
||||||
try {
|
try {
|
||||||
sensorSocket = mesh.connect(sensorUri)
|
sensorSocket = mesh.connect(uri)
|
||||||
postSensor("CultMesh sensors\nconnected $sensorUri\ndocument=mimir.eve_sensor_observation.v1")
|
postSensor("CultMesh sensors\nconnected $uri\ndocument=mimir.eve_sensor_observation.v1")
|
||||||
return@execute
|
return@execute
|
||||||
} catch (ex: Exception) {
|
} catch (ex: Exception) {
|
||||||
postSensor("CultMesh sensors\nwaiting for receiver\n${ex.javaClass.simpleName}: ${ex.message}")
|
postSensor("CultMesh sensors\nwaiting for configured CultMesh sensor receiver\n${ex.javaClass.simpleName}: ${ex.message}")
|
||||||
Thread.sleep(2000)
|
Thread.sleep(2000)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -605,47 +666,6 @@ class MainActivity : Activity(), SensorEventListener {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun refreshProviderCatalog() {
|
|
||||||
workers.execute {
|
|
||||||
runCatching {
|
|
||||||
val url = URL("http://${dashboardUri.host}:${dashboardUri.port}/eve/deck/providers")
|
|
||||||
val connection = (url.openConnection() as HttpURLConnection).apply {
|
|
||||||
connectTimeout = 1500
|
|
||||||
readTimeout = 1500
|
|
||||||
}
|
|
||||||
val raw = connection.inputStream.bufferedReader().use { it.readText() }
|
|
||||||
val providers = JSONObject(raw).optJSONArray("providers") ?: return@runCatching
|
|
||||||
val nodes = mutableListOf<EveDashboardNodeSnapshot>()
|
|
||||||
for (index in 0 until providers.length()) {
|
|
||||||
val provider = providers.getJSONObject(index)
|
|
||||||
val providerId = provider.optString("id", "")
|
|
||||||
if (providerId.isBlank()) continue
|
|
||||||
nodes += EveDashboardNodeSnapshot(
|
|
||||||
id = "provider-${providerId.replace('.', '-')}",
|
|
||||||
label = provider.optString("title", providerId),
|
|
||||||
kind = "dashboard-provider",
|
|
||||||
visible = true,
|
|
||||||
x = 0.0,
|
|
||||||
y = 0.0,
|
|
||||||
z = 0.0,
|
|
||||||
rotation = 0.0,
|
|
||||||
scale = 1.0,
|
|
||||||
width = 0.0,
|
|
||||||
height = 0.0,
|
|
||||||
health = "ok",
|
|
||||||
providerId = providerId,
|
|
||||||
command = "open-provider",
|
|
||||||
endpoint = provider.optString("endpoint", "").ifBlank { null },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
main.post {
|
|
||||||
if (nodes.isNotEmpty()) providerPickerNodes = nodes
|
|
||||||
latestState?.let { renderProviderPicker(it) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun renderElement(element: EveDashboardUiElement, depth: Int): View {
|
private fun renderElement(element: EveDashboardUiElement, depth: Int): View {
|
||||||
element.metric?.let { metric ->
|
element.metric?.let { metric ->
|
||||||
return LinearLayout(this).apply {
|
return LinearLayout(this).apply {
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,8 @@ Current smoke target:
|
||||||
|
|
||||||
- `CultUiInspectorSurface`
|
- `CultUiInspectorSurface`
|
||||||
- custom-painted slider track/fill/thumb
|
- custom-painted slider track/fill/thumb
|
||||||
|
- `EmbeddedSurfaceDemo`, which verifies `surface.slot` /
|
||||||
|
`embeddedDocuments` parsing and rendering from the shared fixture
|
||||||
- golden PNG output through `flutter test --update-goldens`
|
- golden PNG output through `flutter test --update-goldens`
|
||||||
|
|
||||||
Run from the repository root:
|
Run from the repository root:
|
||||||
|
|
|
||||||
|
|
@ -104,6 +104,7 @@ class EveNode {
|
||||||
required this.props,
|
required this.props,
|
||||||
required this.layout,
|
required this.layout,
|
||||||
required this.children,
|
required this.children,
|
||||||
|
required this.embeddedDocuments,
|
||||||
});
|
});
|
||||||
|
|
||||||
final String id;
|
final String id;
|
||||||
|
|
@ -111,6 +112,7 @@ class EveNode {
|
||||||
final Map<String, dynamic> props;
|
final Map<String, dynamic> props;
|
||||||
final Map<String, dynamic> layout;
|
final Map<String, dynamic> layout;
|
||||||
final List<EveNode> children;
|
final List<EveNode> children;
|
||||||
|
final List<EveEmbeddedDocumentSlot> embeddedDocuments;
|
||||||
|
|
||||||
factory EveNode.fromJson(Map<String, dynamic> json) {
|
factory EveNode.fromJson(Map<String, dynamic> json) {
|
||||||
return EveNode(
|
return EveNode(
|
||||||
|
|
@ -121,6 +123,32 @@ class EveNode {
|
||||||
children: ((json['children'] as List<dynamic>?) ?? const [])
|
children: ((json['children'] as List<dynamic>?) ?? const [])
|
||||||
.map((child) => EveNode.fromJson(child as Map<String, dynamic>))
|
.map((child) => EveNode.fromJson(child as Map<String, dynamic>))
|
||||||
.toList(),
|
.toList(),
|
||||||
|
embeddedDocuments: ((json['embeddedDocuments'] as List<dynamic>?) ?? const [])
|
||||||
|
.map((slot) => EveEmbeddedDocumentSlot.fromJson(slot as Map<String, dynamic>))
|
||||||
|
.toList(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class EveEmbeddedDocumentSlot {
|
||||||
|
EveEmbeddedDocumentSlot({
|
||||||
|
required this.slotId,
|
||||||
|
required this.documentId,
|
||||||
|
required this.schemaId,
|
||||||
|
required this.presentationKind,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String slotId;
|
||||||
|
final String documentId;
|
||||||
|
final String schemaId;
|
||||||
|
final String presentationKind;
|
||||||
|
|
||||||
|
factory EveEmbeddedDocumentSlot.fromJson(Map<String, dynamic> json) {
|
||||||
|
return EveEmbeddedDocumentSlot(
|
||||||
|
slotId: (json['slotId'] ?? '').toString(),
|
||||||
|
documentId: (json['documentId'] ?? '').toString(),
|
||||||
|
schemaId: (json['schemaId'] ?? '').toString(),
|
||||||
|
presentationKind: (json['presentationKind'] ?? '').toString(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -250,11 +278,36 @@ class EveNodeView extends StatelessWidget {
|
||||||
return _dialogue();
|
return _dialogue();
|
||||||
case 'rail.actions':
|
case 'rail.actions':
|
||||||
return _actions();
|
return _actions();
|
||||||
|
case 'surface.slot':
|
||||||
|
return _surfaceSlot();
|
||||||
default:
|
default:
|
||||||
return _fallback();
|
return _fallback();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _surfaceSlot() {
|
||||||
|
final slot = node.embeddedDocuments.firstOrNull;
|
||||||
|
final label = node.props['presentationKind']?.toString() ??
|
||||||
|
slot?.presentationKind ??
|
||||||
|
node.props['slotId']?.toString() ??
|
||||||
|
slot?.slotId ??
|
||||||
|
'embedded surface';
|
||||||
|
return DecoratedBox(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: state.tokens.panelAlt.withValues(alpha: 0.65),
|
||||||
|
border: Border.all(color: state.tokens.accent.withValues(alpha: 0.45)),
|
||||||
|
borderRadius: BorderRadius.circular(6),
|
||||||
|
),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(12),
|
||||||
|
child: Text(
|
||||||
|
label,
|
||||||
|
style: TextStyle(color: state.tokens.muted, fontFamily: 'EveParity', fontSize: 12),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Widget _vnStage() {
|
Widget _vnStage() {
|
||||||
final background = node.children.where((child) => child.kind == 'image.background').firstOrNull;
|
final background = node.children.where((child) => child.kind == 'image.background').firstOrNull;
|
||||||
final sceneChildren = node.children.where((child) {
|
final sceneChildren = node.children.where((child) {
|
||||||
|
|
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 27 KiB |
|
|
@ -1,3 +1,6 @@
|
||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:eve_parity/main.dart';
|
import 'package:eve_parity/main.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
|
@ -39,6 +42,40 @@ void main() {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test('parses embedded document slots for CultUI nested surfaces', () {
|
||||||
|
final node = EveNode.fromJson(const {
|
||||||
|
'id': 'test.slot',
|
||||||
|
'kind': 'surface.slot',
|
||||||
|
'props': {'presentationKind': 'inventory.dropdown'},
|
||||||
|
'embeddedDocuments': [
|
||||||
|
{
|
||||||
|
'slotId': 'inventory.dropdown',
|
||||||
|
'documentId': 'cultmesh://test/dropdown',
|
||||||
|
'schemaId': 'gamecult.eve.surface.v1',
|
||||||
|
'presentationKind': 'inventory.dropdown',
|
||||||
|
}
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(node.embeddedDocuments, hasLength(1));
|
||||||
|
expect(node.embeddedDocuments.single.slotId, 'inventory.dropdown');
|
||||||
|
expect(node.embeddedDocuments.single.documentId, 'cultmesh://test/dropdown');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('embedded_surface_fixture_contract', () {
|
||||||
|
final raw = File('../../web/fixtures/cultui-embedded-surface.json').readAsStringSync();
|
||||||
|
final json = jsonDecode(raw) as Map<String, dynamic>;
|
||||||
|
final surface = json['surface'] as Map<String, dynamic>;
|
||||||
|
final root = EveNode.fromJson(surface['root'] as Map<String, dynamic>);
|
||||||
|
final slot = root.children.singleWhere((node) => node.kind == 'surface.slot');
|
||||||
|
|
||||||
|
expect(json['schema'], 'gamecult.eve.surface.v1');
|
||||||
|
expect(slot.embeddedDocuments, hasLength(1));
|
||||||
|
expect(slot.embeddedDocuments.single.slotId, 'inventory.dropdown');
|
||||||
|
expect(slot.embeddedDocuments.single.schemaId, 'gamecult.eve.surface.v1');
|
||||||
|
expect(slot.embeddedDocuments.single.presentationKind, 'inventory.dropdown');
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const fixtureId = String.fromEnvironment('EVE_PARITY_FIXTURE', defaultValue: 'cultui-inspector');
|
const fixtureId = String.fromEnvironment('EVE_PARITY_FIXTURE', defaultValue: 'cultui-inspector');
|
||||||
|
|
|
||||||
|
|
@ -40,3 +40,13 @@ test("preserves slider skin anatomy as retained control parts", () => {
|
||||||
assert.equal(skin.children[3].props.name, "thumb");
|
assert.equal(skin.children[3].props.name, "thumb");
|
||||||
assert.equal(skin.children[3].props.bleed, 3);
|
assert.equal(skin.children[3].props.bleed, 3);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("surface JSON can declare embedded CultMesh document slots", () => {
|
||||||
|
const state = JSON.parse(readFileSync(new URL("./fixtures/cultui-embedded-surface.json", import.meta.url), "utf8"));
|
||||||
|
const slot = state.surface.root.children[1];
|
||||||
|
|
||||||
|
assert.equal(slot.kind, "surface.slot");
|
||||||
|
assert.equal(slot.embeddedDocuments.length, 1);
|
||||||
|
assert.equal(slot.embeddedDocuments[0].slotId, "inventory.dropdown");
|
||||||
|
assert.equal(slot.embeddedDocuments[0].documentId, "cultmesh://demo/inventory/dropdown");
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -212,11 +212,12 @@ function delay(ms) {
|
||||||
function renderSurface(state, source) {
|
function renderSurface(state, source) {
|
||||||
surfaceId.textContent = state.providerId || "surface unknown";
|
surfaceId.textContent = state.providerId || "surface unknown";
|
||||||
surfaceVersion.textContent = `v${state.version ?? "?"}`;
|
surfaceVersion.textContent = `v${state.version ?? "?"}`;
|
||||||
|
const providerId = state.providerId || currentProvider?.providerId || "";
|
||||||
renderEveSurface(state, app, {
|
renderEveSurface(state, app, {
|
||||||
body: document.body,
|
body: document.body,
|
||||||
clientId: liveHermodr ? "hermodr.browser" : "browser.reference",
|
clientId: liveHermodr ? "hermodr.browser" : "browser.reference",
|
||||||
commandSink: publishCommandIntent,
|
commandSink: publishCommandIntent,
|
||||||
documentResolver: liveHermodr ? resolveHermodrDocument : undefined,
|
documentResolver: liveHermodr ? createHermodrDocumentResolver(providerId) : undefined,
|
||||||
assetUrlResolver: liveHermodr ? resolveHermodrAssetUrl : undefined,
|
assetUrlResolver: liveHermodr ? resolveHermodrAssetUrl : undefined,
|
||||||
provider: currentProvider,
|
provider: currentProvider,
|
||||||
source,
|
source,
|
||||||
|
|
@ -224,15 +225,17 @@ function renderSurface(state, source) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function resolveHermodrDocument(request) {
|
function createHermodrDocumentResolver(providerId) {
|
||||||
const providerId = currentProvider?.providerId || "";
|
return async function resolveHermodrDocument(request) {
|
||||||
const params = new URLSearchParams();
|
if (!providerId) return undefined;
|
||||||
params.set("documentId", request.documentId);
|
const params = new URLSearchParams();
|
||||||
if (request.schemaId) params.set("schemaId", request.schemaId);
|
params.set("documentId", request.documentId);
|
||||||
if (request.slotId) params.set("slotId", request.slotId);
|
if (request.schemaId) params.set("schemaId", request.schemaId);
|
||||||
const response = await fetch(`/hermodr/document/${encodeURIComponent(providerId)}?${params.toString()}`, { cache: "no-store" });
|
if (request.slotId) params.set("slotId", request.slotId);
|
||||||
if (!response.ok) throw new Error(await response.text());
|
const response = await fetch(`/hermodr/document/${encodeURIComponent(providerId)}?${params.toString()}`, { cache: "no-store" });
|
||||||
return response.json();
|
if (!response.ok) throw new Error(await response.text());
|
||||||
|
return response.json();
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveHermodrAssetUrl(uri) {
|
function resolveHermodrAssetUrl(uri) {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue