diff --git a/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/api/Device.kt b/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/api/Device.kt index 6b974f2..399f7be 100644 --- a/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/api/Device.kt +++ b/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/api/Device.kt @@ -29,21 +29,25 @@ public interface Device : Closeable, ContextAware, CoroutineScope { public val actionDescriptors: Collection /** - * Get the value of the property or throw error if property in not defined. - * Suspend if property value is not available + * Read physical state of property and update/push notifications if needed. */ - public suspend fun getProperty(propertyName: String): MetaItem + public suspend fun readItem(propertyName: String): MetaItem /** - * Invalidate property and force recalculate + * Get the logical state of property or return null if it is invalid */ - public suspend fun invalidateProperty(propertyName: String) + public fun getItem(propertyName: String): MetaItem? + + /** + * Invalidate property (set logical state to invalid) + */ + public suspend fun invalidate(propertyName: String) /** * Set property [value] for a property with name [propertyName]. * In rare cases could suspend if the [Device] supports command queue and it is full at the moment. */ - public suspend fun setProperty(propertyName: String, value: MetaItem) + public suspend fun writeItem(propertyName: String, value: MetaItem) /** * The [SharedFlow] of property changes @@ -65,9 +69,19 @@ public interface Device : Closeable, ContextAware, CoroutineScope { } } -public suspend fun Device.getState(): Meta = Meta{ - for(descriptor in propertyDescriptors) { - descriptor.name put getProperty(descriptor.name) + +/** + * Get the logical state of property or suspend to read the physical value. + */ +public suspend fun Device.getOrReadItem(propertyName: String): MetaItem = + getItem(propertyName) ?: readItem(propertyName) + +/** + * Get a snapshot of logical state of the device + */ +public fun Device.getProperties(): Meta = Meta { + for (descriptor in propertyDescriptors) { + descriptor.name put getItem(descriptor.name) } } diff --git a/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/api/DeviceHub.kt b/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/api/DeviceHub.kt index 065c1b7..e77c924 100644 --- a/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/api/DeviceHub.kt +++ b/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/api/DeviceHub.kt @@ -58,11 +58,11 @@ public fun DeviceHub.getOrNull(nameString: String): Device? = getOrNull(nameStri public operator fun DeviceHub.get(nameString: String): Device = getOrNull(nameString) ?: error("Device with name $nameString not found in $this") -public suspend fun DeviceHub.getProperty(deviceName: Name, propertyName: String): MetaItem = - this[deviceName].getProperty(propertyName) +public suspend fun DeviceHub.readItem(deviceName: Name, propertyName: String): MetaItem = + this[deviceName].readItem(propertyName) -public suspend fun DeviceHub.setProperty(deviceName: Name, propertyName: String, value: MetaItem) { - this[deviceName].setProperty(propertyName, value) +public suspend fun DeviceHub.writeItem(deviceName: Name, propertyName: String, value: MetaItem) { + this[deviceName].writeItem(propertyName, value) } public suspend fun DeviceHub.execute(deviceName: Name, command: String, argument: MetaItem?): MetaItem? = diff --git a/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/base/DeviceBase.kt b/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/base/DeviceBase.kt index 0033f0b..037cdaa 100644 --- a/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/base/DeviceBase.kt +++ b/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/base/DeviceBase.kt @@ -152,14 +152,17 @@ public abstract class DeviceBase(final override val context: Context) : Device { _actions[name] = action } - override suspend fun getProperty(propertyName: String): MetaItem = + override suspend fun readItem(propertyName: String): MetaItem = (_properties[propertyName] ?: error("Property with name $propertyName not defined")).read() - override suspend fun invalidateProperty(propertyName: String) { + override fun getItem(propertyName: String): MetaItem?= + (_properties[propertyName] ?: error("Property with name $propertyName not defined")).value + + override suspend fun invalidate(propertyName: String) { (_properties[propertyName] ?: error("Property with name $propertyName not defined")).invalidate() } - override suspend fun setProperty(propertyName: String, value: MetaItem) { + override suspend fun writeItem(propertyName: String, value: MetaItem) { (_properties[propertyName] as? DeviceProperty ?: error("Property with name $propertyName not defined")).write( value ) diff --git a/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/controllers/DeviceController.kt b/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/controllers/DeviceController.kt index a478d3c..1dfb248 100644 --- a/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/controllers/DeviceController.kt +++ b/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/controllers/DeviceController.kt @@ -67,7 +67,7 @@ public class DeviceController( is PropertyGetMessage -> { PropertyChangedMessage( property = request.property, - value = device.getProperty(request.property), + value = device.getOrReadItem(request.property), sourceDevice = deviceTarget, targetDevice = request.sourceDevice ) @@ -75,13 +75,13 @@ public class DeviceController( is PropertySetMessage -> { if (request.value == null) { - device.invalidateProperty(request.property) + device.invalidate(request.property) } else { - device.setProperty(request.property, request.value) + device.writeItem(request.property, request.value) } PropertyChangedMessage( property = request.property, - value = device.getProperty(request.property), + value = device.getOrReadItem(request.property), sourceDevice = deviceTarget, targetDevice = request.sourceDevice ) diff --git a/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/properties/DeviceBySpec.kt b/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/properties/DeviceBySpec.kt index 658884a..1fe92b1 100644 --- a/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/properties/DeviceBySpec.kt +++ b/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/properties/DeviceBySpec.kt @@ -1,8 +1,10 @@ package ru.mipt.npm.controls.properties -import kotlinx.coroutines.* +import kotlinx.coroutines.Job +import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import ru.mipt.npm.controls.api.ActionDescriptor @@ -21,6 +23,7 @@ import kotlin.reflect.KProperty /** * @param D recursive self-type for properties and actions */ +@OptIn(InternalDeviceAPI::class) public open class DeviceBySpec>( public val spec: DeviceSpec, context: Context = Global, @@ -55,11 +58,9 @@ public open class DeviceBySpec>( internal val self: D get() = this as D - internal fun getLogicalState(propertyName: String): MetaItem? = logicalState[propertyName] - private val stateLock = Mutex() - internal suspend fun updateLogical(propertyName: String, value: MetaItem?) { + private suspend fun updateLogical(propertyName: String, value: MetaItem?) { if (value != logicalState[propertyName]) { stateLock.withLock { logicalState[propertyName] = value @@ -74,27 +75,26 @@ public open class DeviceBySpec>( * Force read physical value and push an update if it is changed. It does not matter if logical state is present. * The logical state is updated after read */ - public suspend fun readProperty(propertyName: String): MetaItem { + override suspend fun readItem(propertyName: String): MetaItem { val newValue = properties[propertyName]?.readItem(self) ?: error("A property with name $propertyName is not registered in $this") updateLogical(propertyName, newValue) return newValue } - override suspend fun getProperty(propertyName: String): MetaItem = - logicalState[propertyName] ?: readProperty(propertyName) + override fun getItem(propertyName: String): MetaItem? = logicalState[propertyName] - override suspend fun invalidateProperty(propertyName: String) { + override suspend fun invalidate(propertyName: String) { stateLock.withLock { logicalState.remove(propertyName) } } - override suspend fun setProperty(propertyName: String, value: MetaItem): Unit { + override suspend fun writeItem(propertyName: String, value: MetaItem): Unit { //If there is a physical property with given name, invalidate logical property and write physical one (properties[propertyName] as? WritableDevicePropertySpec)?.let { it.writeItem(self, value) - invalidateProperty(propertyName) + invalidate(propertyName) } ?: run { updateLogical(propertyName, value) } @@ -113,36 +113,44 @@ public open class DeviceBySpec>( ): ReadWriteProperty = observable(initialValue) { property: KProperty<*>, oldValue: T, newValue: T -> if (oldValue != newValue) { launch { - invalidateProperty(property.name) + invalidate(property.name) _propertyFlow.emit(property.name to converter.objectToMetaItem(newValue)) } } } - public suspend fun DevicePropertySpec.read(): T = read(self) + /** + * Read typed value and update/push event if needed + */ + public suspend fun DevicePropertySpec.read(): T { + val res = read(self) + updateLogical(name, converter.objectToMetaItem(res)) + return res + } + + public fun DevicePropertySpec.get(): T? = getItem(name)?.let(converter::itemToObject) + + /** + * Write typed property state and invalidate logical state + */ + public suspend fun WritableDevicePropertySpec.write(value: T) { + write(self, value) + invalidate(name) + } override fun close() { - with(spec){ self.onShutdown() } + with(spec) { self.onShutdown() } super.close() } } -public suspend fun , T : Any> D.getSuspend( +public suspend fun , T : Any> D.read( propertySpec: DevicePropertySpec -): T = propertySpec.read(this@getSuspend).also { - updateLogical(propertySpec.name, propertySpec.converter.objectToMetaItem(it)) +): T = propertySpec.read() + +public fun , T : Any> D.write( + propertySpec: WritableDevicePropertySpec, + value: T +): Job = launch { + propertySpec.write(value) } - - -public fun , T : Any> D.getAsync( - propertySpec: DevicePropertySpec -): Deferred = async { - getSuspend(propertySpec) -} - -public operator fun , T : Any> D.set(propertySpec: WritableDevicePropertySpec, value: T) { - launch { - propertySpec.write(this@set, value) - invalidateProperty(propertySpec.name) - } -} \ No newline at end of file diff --git a/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/properties/DevicePropertySpec.kt b/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/properties/DevicePropertySpec.kt index 38fcb9f..81adc7f 100644 --- a/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/properties/DevicePropertySpec.kt +++ b/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/properties/DevicePropertySpec.kt @@ -8,6 +8,13 @@ import space.kscience.dataforge.meta.transformations.MetaConverter import space.kscience.dataforge.meta.transformations.nullableItemToObject import space.kscience.dataforge.meta.transformations.nullableObjectToMetaItem + +/** + * This API is internal and should not be used in user code + */ +@RequiresOptIn +public annotation class InternalDeviceAPI + //TODO relax T restriction after DF 0.4.4 public interface DevicePropertySpec { /** @@ -28,9 +35,11 @@ public interface DevicePropertySpec { /** * Read physical value from the given [device] */ + @InternalDeviceAPI public suspend fun read(device: D): T } +@OptIn(InternalDeviceAPI::class) public suspend fun DevicePropertySpec.readItem(device: D): MetaItem = converter.objectToMetaItem(read(device)) @@ -39,9 +48,11 @@ public interface WritableDevicePropertySpec : DeviceProp /** * Write physical value to a device */ + @InternalDeviceAPI public suspend fun write(device: D, value: T) } +@OptIn(InternalDeviceAPI::class) public suspend fun WritableDevicePropertySpec.writeItem(device: D, item: MetaItem) { write(device, converter.itemToObject(item)) } diff --git a/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/properties/DeviceSpec.kt b/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/properties/DeviceSpec.kt index 33ad3e9..17d52c7 100644 --- a/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/properties/DeviceSpec.kt +++ b/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/properties/DeviceSpec.kt @@ -13,6 +13,7 @@ import kotlin.reflect.KMutableProperty1 import kotlin.reflect.KProperty import kotlin.reflect.KProperty1 +@OptIn(InternalDeviceAPI::class) public abstract class DeviceSpec>( private val buildDevice: () -> D ) : Factory { diff --git a/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/properties/deviceExtensions.kt b/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/properties/deviceExtensions.kt new file mode 100644 index 0000000..582c8a7 --- /dev/null +++ b/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/properties/deviceExtensions.kt @@ -0,0 +1,32 @@ +package ru.mipt.npm.controls.properties + +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch +import kotlin.time.Duration + +/** + * Perform a recurring asynchronous read action and return a flow of results. + * The flow is lazy so action is not performed unless flow is consumed. + * The flow uses called context. In order to call it on device context, use `flowOn(coroutineContext)`. + * + * The flow is canceled when the device scope is canceled + */ +public fun , R> D.readRecurring(interval: Duration, reader: suspend D.() -> R): Flow = flow { + while (isActive) { + kotlinx.coroutines.delay(interval) + emit(reader()) + } +} + +/** + * Do a recurring task on a device. The task could + */ +public fun > D.doRecurring(interval: Duration, task: suspend D.() -> Unit): Job = launch { + while (isActive) { + kotlinx.coroutines.delay(interval) + task() + } +} \ No newline at end of file diff --git a/controls-core/src/jvmMain/kotlin/ru/mipt/npm/controls/properties/getDeviceProperty.kt b/controls-core/src/jvmMain/kotlin/ru/mipt/npm/controls/properties/getDeviceProperty.kt index 20b1ded..3be61d6 100644 --- a/controls-core/src/jvmMain/kotlin/ru/mipt/npm/controls/properties/getDeviceProperty.kt +++ b/controls-core/src/jvmMain/kotlin/ru/mipt/npm/controls/properties/getDeviceProperty.kt @@ -1,13 +1,10 @@ package ru.mipt.npm.controls.properties import kotlinx.coroutines.runBlocking -import ru.mipt.npm.controls.api.PropertyDescriptor -import space.kscience.dataforge.meta.transformations.MetaConverter -import kotlin.reflect.KFunction /** * Blocking property get call */ public operator fun , T : Any> D.get( propertySpec: DevicePropertySpec -): T = runBlocking { getAsync(propertySpec).await() } \ No newline at end of file +): T = runBlocking { read(propertySpec) } \ No newline at end of file diff --git a/controls-magix-client/src/commonMain/kotlin/ru/mipt/npm/controls/client/tangoMagix.kt b/controls-magix-client/src/commonMain/kotlin/ru/mipt/npm/controls/client/tangoMagix.kt index f5fd340..ae2672f 100644 --- a/controls-magix-client/src/commonMain/kotlin/ru/mipt/npm/controls/client/tangoMagix.kt +++ b/controls-magix-client/src/commonMain/kotlin/ru/mipt/npm/controls/client/tangoMagix.kt @@ -6,6 +6,7 @@ import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import kotlinx.serialization.Serializable import ru.mipt.npm.controls.api.get +import ru.mipt.npm.controls.api.getOrReadItem import ru.mipt.npm.controls.controllers.DeviceManager import ru.mipt.npm.magix.api.MagixEndpoint import ru.mipt.npm.magix.api.MagixMessage @@ -83,7 +84,7 @@ public fun DeviceManager.launchTangoMagix( val device = get(request.payload.device) when (request.payload.action) { TangoAction.read -> { - val value = device.getProperty(request.payload.name) + val value = device.getOrReadItem(request.payload.name) respond(request) { requestPayload -> requestPayload.copy( value = value, @@ -93,10 +94,10 @@ public fun DeviceManager.launchTangoMagix( } TangoAction.write -> { request.payload.value?.let { value -> - device.setProperty(request.payload.name, value) + device.writeItem(request.payload.name, value) } //wait for value to be written and return final state - val value = device.getProperty(request.payload.name) + val value = device.getOrReadItem(request.payload.name) respond(request) { requestPayload -> requestPayload.copy( value = value, diff --git a/demo/src/main/kotlin/ru/mipt/npm/controls/demo/DemoDevice.kt b/demo/src/main/kotlin/ru/mipt/npm/controls/demo/DemoDevice.kt index b678ed2..308bc48 100644 --- a/demo/src/main/kotlin/ru/mipt/npm/controls/demo/DemoDevice.kt +++ b/demo/src/main/kotlin/ru/mipt/npm/controls/demo/DemoDevice.kt @@ -1,17 +1,16 @@ package ru.mipt.npm.controls.demo -import kotlinx.coroutines.delay -import kotlinx.coroutines.isActive -import kotlinx.coroutines.launch import ru.mipt.npm.controls.properties.* import space.kscience.dataforge.meta.Meta import space.kscience.dataforge.meta.transformations.MetaConverter import java.time.Instant +import kotlin.time.Duration +import kotlin.time.ExperimentalTime class DemoDevice : DeviceBySpec(DemoDevice) { var timeScale by state(5000.0) - var sinScale by state( 1.0) + var sinScale by state(1.0) var cosScale by state(1.0) companion object : DeviceSpec(::DemoDevice) { @@ -34,8 +33,8 @@ class DemoDevice : DeviceBySpec(DemoDevice) { Meta { val time = Instant.now() "time" put time.toEpochMilli() - "x" put getSuspend(sin) - "y" put getSuspend(cos) + "x" put read(sin) + "y" put read(cos) } } @@ -46,13 +45,11 @@ class DemoDevice : DeviceBySpec(DemoDevice) { null } + @OptIn(ExperimentalTime::class) override fun DemoDevice.onStartup() { - launch { - while(isActive){ - delay(50) - sin.read() - cos.read() - } + doRecurring(Duration.milliseconds(50)){ + sin.read() + cos.read() } } }