Make remote device connection ask for descriptors before start

This commit is contained in:
Alexander Nozik 2024-05-17 23:01:20 +03:00
parent ee83f81a04
commit e5088ac8e4
6 changed files with 59 additions and 25 deletions

View File

@ -100,8 +100,8 @@ public interface Device : ContextAware, CoroutineScope {
* Close and terminate the device. This function does not wait for the device to be closed. * Close and terminate the device. This function does not wait for the device to be closed.
*/ */
public suspend fun stop() { public suspend fun stop() {
coroutineContext[Job]?.cancel("The device is closed")
logger.info { "Device $this is closed" } logger.info { "Device $this is closed" }
cancel("The device is closed")
} }
public val lifecycleState: DeviceLifecycleState public val lifecycleState: DeviceLifecycleState

View File

@ -17,11 +17,15 @@ kscience {
useSerialization { useSerialization {
json() json()
} }
dependencies { commonMain {
api(projects.magix.magixApi) api(projects.magix.magixApi)
api(projects.controlsCore) api(projects.controlsCore)
api(libs.uuid) api(libs.uuid)
} }
jvmTest{
implementation(spclibs.logback.classic)
}
} }
readme { readme {

View File

@ -1,8 +1,11 @@
package space.kscience.controls.client package space.kscience.controls.client
import com.benasher44.uuid.uuid4 import com.benasher44.uuid.uuid4
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
import space.kscience.controls.api.* import space.kscience.controls.api.*
@ -23,9 +26,11 @@ private fun stringUID() = uuid4().leastSignificantBits.toString(16)
/** /**
* A remote accessible device that relies on connection via Magix * A remote accessible device that relies on connection via Magix
*/ */
public class DeviceClient( public class DeviceClient internal constructor(
override val context: Context, override val context: Context,
private val deviceName: Name, private val deviceName: Name,
override val propertyDescriptors: Collection<PropertyDescriptor>,
override val actionDescriptors: Collection<ActionDescriptor>,
incomingFlow: Flow<DeviceMessage>, incomingFlow: Flow<DeviceMessage>,
private val send: suspend (DeviceMessage) -> Unit, private val send: suspend (DeviceMessage) -> Unit,
) : CachingDevice { ) : CachingDevice {
@ -37,12 +42,6 @@ public class DeviceClient(
private val propertyCache = HashMap<String, Meta>() private val propertyCache = HashMap<String, Meta>()
override var propertyDescriptors: Collection<PropertyDescriptor> = emptyList()
private set
override var actionDescriptors: Collection<ActionDescriptor> = emptyList()
private set
private val flowInternal = incomingFlow.filter { private val flowInternal = incomingFlow.filter {
it.sourceDevice == deviceName it.sourceDevice == deviceName
}.shareIn(this, started = SharingStarted.Eagerly).also { }.shareIn(this, started = SharingStarted.Eagerly).also {
@ -52,11 +51,6 @@ public class DeviceClient(
propertyCache[message.property] = message.value propertyCache[message.property] = message.value
} }
is DescriptionMessage -> mutex.withLock {
propertyDescriptors = message.properties
actionDescriptors = message.actions
}
else -> { else -> {
//ignore //ignore
} }
@ -112,14 +106,38 @@ public class DeviceClient(
* @param targetEndpointName the name of endpoint in Magix to connect to * @param targetEndpointName the name of endpoint in Magix to connect to
* @param deviceName the name of device within endpoint * @param deviceName the name of device within endpoint
*/ */
public fun MagixEndpoint.remoteDevice( public suspend fun MagixEndpoint.remoteDevice(
context: Context, context: Context,
sourceEndpointName: String, sourceEndpointName: String,
targetEndpointName: String, targetEndpointName: String,
deviceName: Name, deviceName: Name,
): DeviceClient { ): DeviceClient = coroutineScope{
val subscription = subscribe(DeviceManager.magixFormat, originFilter = listOf(targetEndpointName)).map { it.second } val subscription = subscribe(DeviceManager.magixFormat, originFilter = listOf(targetEndpointName)).map { it.second }
return DeviceClient(context, deviceName, subscription) {
val deferredDescriptorMessage = CompletableDeferred<DescriptionMessage>()
launch {
deferredDescriptorMessage.complete(subscription.filterIsInstance<DescriptionMessage>().first())
}
send(
format = DeviceManager.magixFormat,
payload = GetDescriptionMessage(targetDevice = deviceName),
source = sourceEndpointName,
target = targetEndpointName,
id = stringUID()
)
val descriptionMessage = deferredDescriptorMessage.await()
DeviceClient(
context = context,
deviceName = deviceName,
propertyDescriptors = descriptionMessage.properties,
actionDescriptors = descriptionMessage.actions,
incomingFlow = subscription
) {
send( send(
format = DeviceManager.magixFormat, format = DeviceManager.magixFormat,
payload = it, payload = it,
@ -130,6 +148,8 @@ public fun MagixEndpoint.remoteDevice(
} }
} }
//public fun MagixEndpoint.remoteDeviceHub()
/** /**
* Subscribe on specific property of a device without creating a device * Subscribe on specific property of a device without creating a device
*/ */

View File

@ -1,5 +1,6 @@
package space.kscience.controls.client package space.kscience.controls.client
import com.benasher44.uuid.uuid4
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
@ -29,7 +30,7 @@ public val DeviceManager.Companion.magixFormat: MagixFormat<DeviceMessage> get()
internal fun generateId(request: MagixMessage): String = if (request.id != null) { internal fun generateId(request: MagixMessage): String = if (request.id != null) {
"${request.id}.response" "${request.id}.response"
} else { } else {
"controls[${request.payload.hashCode().toUInt().toString(16)}]" uuid4().leastSignificantBits.toULong().toString(16)
} }
/** /**

View File

@ -1,9 +1,12 @@
package space.kscience.controls.client package space.kscience.controls.client
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import space.kscience.controls.api.DeviceMessage
import space.kscience.controls.manager.DeviceManager import space.kscience.controls.manager.DeviceManager
import space.kscience.controls.manager.install import space.kscience.controls.manager.install
import space.kscience.controls.manager.respondMessage import space.kscience.controls.manager.respondMessage
@ -46,7 +49,7 @@ internal class RemoteDeviceConnect {
} }
@Test @Test
fun wrapper() = runTest { fun deviceClient() = runTest {
val context = Context { val context = Context {
plugin(DeviceManager) plugin(DeviceManager)
} }
@ -56,11 +59,15 @@ internal class RemoteDeviceConnect {
val virtualMagixEndpoint = object : MagixEndpoint { val virtualMagixEndpoint = object : MagixEndpoint {
override fun subscribe(filter: MagixMessageFilter): Flow<MagixMessage> = device.messageFlow.map { private val additionalMessages = MutableSharedFlow<DeviceMessage>(10)
override fun subscribe(
filter: MagixMessageFilter,
): Flow<MagixMessage> = merge(device.messageFlow, additionalMessages).map {
MagixMessage( MagixMessage(
format = DeviceManager.magixFormat.defaultFormat, format = DeviceManager.magixFormat.defaultFormat,
payload = MagixEndpoint.magixJson.encodeToJsonElement(DeviceManager.magixFormat.serializer, it), payload = MagixEndpoint.magixJson.encodeToJsonElement(DeviceManager.magixFormat.serializer, it),
sourceEndpoint = "source", sourceEndpoint = "device",
) )
} }
@ -68,16 +75,18 @@ internal class RemoteDeviceConnect {
device.respondMessage( device.respondMessage(
Name.EMPTY, Name.EMPTY,
Json.decodeFromJsonElement(DeviceManager.magixFormat.serializer, message.payload) Json.decodeFromJsonElement(DeviceManager.magixFormat.serializer, message.payload)
) )?.let {
additionalMessages.emit(it)
}
} }
override fun close() { override fun close() {
// //
} }
} }
val remoteDevice = virtualMagixEndpoint.remoteDevice(context, "client", "device", Name.EMPTY)
val remoteDevice = virtualMagixEndpoint.remoteDevice(context, "source", "target", Name.EMPTY)
assertContains(0.0..1.0, remoteDevice.read(TestDevice.value)) assertContains(0.0..1.0, remoteDevice.read(TestDevice.value))
} }
} }

View File

@ -56,6 +56,6 @@ public suspend fun <T> MagixEndpoint.send(
parentId = parentId, parentId = parentId,
user = user user = user
) )
broadcast(message) send(message)
} }