Refactoring properties getters

This commit is contained in:
Alexander Nozik 2021-06-26 20:34:40 +03:00
parent fe3958fd08
commit 596e3a0cfc
11 changed files with 133 additions and 69 deletions

View File

@ -29,21 +29,25 @@ public interface Device : Closeable, ContextAware, CoroutineScope {
public val actionDescriptors: Collection<ActionDescriptor> public val actionDescriptors: Collection<ActionDescriptor>
/** /**
* Get the value of the property or throw error if property in not defined. * Read physical state of property and update/push notifications if needed.
* Suspend if property value is not available
*/ */
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]. * 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. * 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 * 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)
} }
} }

View File

@ -58,11 +58,11 @@ public fun DeviceHub.getOrNull(nameString: String): Device? = getOrNull(nameStri
public operator fun DeviceHub.get(nameString: String): Device = public operator fun DeviceHub.get(nameString: String): Device =
getOrNull(nameString) ?: error("Device with name $nameString not found in $this") getOrNull(nameString) ?: error("Device with name $nameString not found in $this")
public suspend fun DeviceHub.getProperty(deviceName: Name, propertyName: String): MetaItem = public suspend fun DeviceHub.readItem(deviceName: Name, propertyName: String): MetaItem =
this[deviceName].getProperty(propertyName) this[deviceName].readItem(propertyName)
public suspend fun DeviceHub.setProperty(deviceName: Name, propertyName: String, value: MetaItem) { public suspend fun DeviceHub.writeItem(deviceName: Name, propertyName: String, value: MetaItem) {
this[deviceName].setProperty(propertyName, value) this[deviceName].writeItem(propertyName, value)
} }
public suspend fun DeviceHub.execute(deviceName: Name, command: String, argument: MetaItem?): MetaItem? = public suspend fun DeviceHub.execute(deviceName: Name, command: String, argument: MetaItem?): MetaItem? =

View File

@ -152,14 +152,17 @@ public abstract class DeviceBase(final override val context: Context) : Device {
_actions[name] = action _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() (_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() (_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( (_properties[propertyName] as? DeviceProperty ?: error("Property with name $propertyName not defined")).write(
value value
) )

View File

@ -67,7 +67,7 @@ public class DeviceController(
is PropertyGetMessage -> { is PropertyGetMessage -> {
PropertyChangedMessage( PropertyChangedMessage(
property = request.property, property = request.property,
value = device.getProperty(request.property), value = device.getOrReadItem(request.property),
sourceDevice = deviceTarget, sourceDevice = deviceTarget,
targetDevice = request.sourceDevice targetDevice = request.sourceDevice
) )
@ -75,13 +75,13 @@ public class DeviceController(
is PropertySetMessage -> { is PropertySetMessage -> {
if (request.value == null) { if (request.value == null) {
device.invalidateProperty(request.property) device.invalidate(request.property)
} else { } else {
device.setProperty(request.property, request.value) device.writeItem(request.property, request.value)
} }
PropertyChangedMessage( PropertyChangedMessage(
property = request.property, property = request.property,
value = device.getProperty(request.property), value = device.getOrReadItem(request.property),
sourceDevice = deviceTarget, sourceDevice = deviceTarget,
targetDevice = request.sourceDevice targetDevice = request.sourceDevice
) )

View File

@ -1,8 +1,10 @@
package ru.mipt.npm.controls.properties 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.MutableSharedFlow
import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.SharedFlow
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 ru.mipt.npm.controls.api.ActionDescriptor import ru.mipt.npm.controls.api.ActionDescriptor
@ -21,6 +23,7 @@ import kotlin.reflect.KProperty
/** /**
* @param D recursive self-type for properties and actions * @param D recursive self-type for properties and actions
*/ */
@OptIn(InternalDeviceAPI::class)
public open class DeviceBySpec<D : DeviceBySpec<D>>( public open class DeviceBySpec<D : DeviceBySpec<D>>(
public val spec: DeviceSpec<D>, public val spec: DeviceSpec<D>,
context: Context = Global, context: Context = Global,
@ -55,11 +58,9 @@ public open class DeviceBySpec<D : DeviceBySpec<D>>(
internal val self: D internal val self: D
get() = this as D get() = this as D
internal fun getLogicalState(propertyName: String): MetaItem? = logicalState[propertyName]
private val stateLock = Mutex() private val stateLock = Mutex()
internal suspend fun updateLogical(propertyName: String, value: MetaItem?) { private suspend fun updateLogical(propertyName: String, value: MetaItem?) {
if (value != logicalState[propertyName]) { if (value != logicalState[propertyName]) {
stateLock.withLock { stateLock.withLock {
logicalState[propertyName] = value logicalState[propertyName] = value
@ -74,27 +75,26 @@ public open class DeviceBySpec<D : DeviceBySpec<D>>(
* Force read physical value and push an update if it is changed. It does not matter if logical state is present. * 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 * 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) val newValue = properties[propertyName]?.readItem(self)
?: error("A property with name $propertyName is not registered in $this") ?: error("A property with name $propertyName is not registered in $this")
updateLogical(propertyName, newValue) updateLogical(propertyName, newValue)
return newValue return newValue
} }
override suspend fun getProperty(propertyName: String): MetaItem = override fun getItem(propertyName: String): MetaItem? = logicalState[propertyName]
logicalState[propertyName] ?: readProperty(propertyName)
override suspend fun invalidateProperty(propertyName: String) { override suspend fun invalidate(propertyName: String) {
stateLock.withLock { stateLock.withLock {
logicalState.remove(propertyName) 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 //If there is a physical property with given name, invalidate logical property and write physical one
(properties[propertyName] as? WritableDevicePropertySpec<D, out Any>)?.let { (properties[propertyName] as? WritableDevicePropertySpec<D, out Any>)?.let {
it.writeItem(self, value) it.writeItem(self, value)
invalidateProperty(propertyName) invalidate(propertyName)
} ?: run { } ?: run {
updateLogical(propertyName, value) updateLogical(propertyName, value)
} }
@ -113,36 +113,44 @@ public open class DeviceBySpec<D : DeviceBySpec<D>>(
): ReadWriteProperty<D, T> = observable(initialValue) { property: KProperty<*>, oldValue: T, newValue: T -> ): ReadWriteProperty<D, T> = observable(initialValue) { property: KProperty<*>, oldValue: T, newValue: T ->
if (oldValue != newValue) { if (oldValue != newValue) {
launch { launch {
invalidateProperty(property.name) invalidate(property.name)
_propertyFlow.emit(property.name to converter.objectToMetaItem(newValue)) _propertyFlow.emit(property.name to converter.objectToMetaItem(newValue))
} }
} }
} }
public suspend fun <T : Any> DevicePropertySpec<D, T>.read(): T = read(self) /**
* Read typed value and update/push event if needed
*/
public suspend fun <T : Any> DevicePropertySpec<D, T>.read(): T {
val res = read(self)
updateLogical(name, converter.objectToMetaItem(res))
return res
}
public fun <T : Any> DevicePropertySpec<D, T>.get(): T? = getItem(name)?.let(converter::itemToObject)
/**
* Write typed property state and invalidate logical state
*/
public suspend fun <T : Any> WritableDevicePropertySpec<D, T>.write(value: T) {
write(self, value)
invalidate(name)
}
override fun close() { override fun close() {
with(spec){ self.onShutdown() } with(spec) { self.onShutdown() }
super.close() super.close()
} }
} }
public suspend fun <D : DeviceBySpec<D>, T : Any> D.getSuspend( public suspend fun <D : DeviceBySpec<D>, T : Any> D.read(
propertySpec: DevicePropertySpec<D, T> propertySpec: DevicePropertySpec<D, T>
): T = propertySpec.read(this@getSuspend).also { ): T = propertySpec.read()
updateLogical(propertySpec.name, propertySpec.converter.objectToMetaItem(it))
public fun <D : DeviceBySpec<D>, T : Any> D.write(
propertySpec: WritableDevicePropertySpec<D, T>,
value: T
): Job = launch {
propertySpec.write(value)
} }
public fun <D : DeviceBySpec<D>, T : Any> D.getAsync(
propertySpec: DevicePropertySpec<D, T>
): Deferred<T> = async {
getSuspend(propertySpec)
}
public operator fun <D : DeviceBySpec<D>, T : Any> D.set(propertySpec: WritableDevicePropertySpec<D, T>, value: T) {
launch {
propertySpec.write(this@set, value)
invalidateProperty(propertySpec.name)
}
}

View File

@ -8,6 +8,13 @@ import space.kscience.dataforge.meta.transformations.MetaConverter
import space.kscience.dataforge.meta.transformations.nullableItemToObject import space.kscience.dataforge.meta.transformations.nullableItemToObject
import space.kscience.dataforge.meta.transformations.nullableObjectToMetaItem 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 //TODO relax T restriction after DF 0.4.4
public interface DevicePropertySpec<in D : Device, T : Any> { public interface DevicePropertySpec<in D : Device, T : Any> {
/** /**
@ -28,9 +35,11 @@ public interface DevicePropertySpec<in D : Device, T : Any> {
/** /**
* Read physical value from the given [device] * Read physical value from the given [device]
*/ */
@InternalDeviceAPI
public suspend fun read(device: D): T public suspend fun read(device: D): T
} }
@OptIn(InternalDeviceAPI::class)
public suspend fun <D : Device, T : Any> DevicePropertySpec<D, T>.readItem(device: D): MetaItem = public suspend fun <D : Device, T : Any> DevicePropertySpec<D, T>.readItem(device: D): MetaItem =
converter.objectToMetaItem(read(device)) converter.objectToMetaItem(read(device))
@ -39,9 +48,11 @@ public interface WritableDevicePropertySpec<in D : Device, T : Any> : DeviceProp
/** /**
* Write physical value to a device * Write physical value to a device
*/ */
@InternalDeviceAPI
public suspend fun write(device: D, value: T) public suspend fun write(device: D, value: T)
} }
@OptIn(InternalDeviceAPI::class)
public suspend fun <D : Device, T : Any> WritableDevicePropertySpec<D, T>.writeItem(device: D, item: MetaItem) { public suspend fun <D : Device, T : Any> WritableDevicePropertySpec<D, T>.writeItem(device: D, item: MetaItem) {
write(device, converter.itemToObject(item)) write(device, converter.itemToObject(item))
} }

View File

@ -13,6 +13,7 @@ import kotlin.reflect.KMutableProperty1
import kotlin.reflect.KProperty import kotlin.reflect.KProperty
import kotlin.reflect.KProperty1 import kotlin.reflect.KProperty1
@OptIn(InternalDeviceAPI::class)
public abstract class DeviceSpec<D : DeviceBySpec<D>>( public abstract class DeviceSpec<D : DeviceBySpec<D>>(
private val buildDevice: () -> D private val buildDevice: () -> D
) : Factory<D> { ) : Factory<D> {

View File

@ -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 <D : DeviceBySpec<D>, R> D.readRecurring(interval: Duration, reader: suspend D.() -> R): Flow<R> = flow {
while (isActive) {
kotlinx.coroutines.delay(interval)
emit(reader())
}
}
/**
* Do a recurring task on a device. The task could
*/
public fun <D : DeviceBySpec<D>> D.doRecurring(interval: Duration, task: suspend D.() -> Unit): Job = launch {
while (isActive) {
kotlinx.coroutines.delay(interval)
task()
}
}

View File

@ -1,13 +1,10 @@
package ru.mipt.npm.controls.properties package ru.mipt.npm.controls.properties
import kotlinx.coroutines.runBlocking 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 * Blocking property get call
*/ */
public operator fun <D : DeviceBySpec<D>, T : Any> D.get( public operator fun <D : DeviceBySpec<D>, T : Any> D.get(
propertySpec: DevicePropertySpec<D, T> propertySpec: DevicePropertySpec<D, T>
): T = runBlocking { getAsync(propertySpec).await() } ): T = runBlocking { read(propertySpec) }

View File

@ -6,6 +6,7 @@ import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import ru.mipt.npm.controls.api.get 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.controls.controllers.DeviceManager
import ru.mipt.npm.magix.api.MagixEndpoint import ru.mipt.npm.magix.api.MagixEndpoint
import ru.mipt.npm.magix.api.MagixMessage import ru.mipt.npm.magix.api.MagixMessage
@ -83,7 +84,7 @@ public fun DeviceManager.launchTangoMagix(
val device = get(request.payload.device) val device = get(request.payload.device)
when (request.payload.action) { when (request.payload.action) {
TangoAction.read -> { TangoAction.read -> {
val value = device.getProperty(request.payload.name) val value = device.getOrReadItem(request.payload.name)
respond(request) { requestPayload -> respond(request) { requestPayload ->
requestPayload.copy( requestPayload.copy(
value = value, value = value,
@ -93,10 +94,10 @@ public fun DeviceManager.launchTangoMagix(
} }
TangoAction.write -> { TangoAction.write -> {
request.payload.value?.let { value -> 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 //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 -> respond(request) { requestPayload ->
requestPayload.copy( requestPayload.copy(
value = value, value = value,

View File

@ -1,17 +1,16 @@
package ru.mipt.npm.controls.demo 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 ru.mipt.npm.controls.properties.*
import space.kscience.dataforge.meta.Meta import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.transformations.MetaConverter import space.kscience.dataforge.meta.transformations.MetaConverter
import java.time.Instant import java.time.Instant
import kotlin.time.Duration
import kotlin.time.ExperimentalTime
class DemoDevice : DeviceBySpec<DemoDevice>(DemoDevice) { class DemoDevice : DeviceBySpec<DemoDevice>(DemoDevice) {
var timeScale by state(5000.0) var timeScale by state(5000.0)
var sinScale by state( 1.0) var sinScale by state(1.0)
var cosScale by state(1.0) var cosScale by state(1.0)
companion object : DeviceSpec<DemoDevice>(::DemoDevice) { companion object : DeviceSpec<DemoDevice>(::DemoDevice) {
@ -34,8 +33,8 @@ class DemoDevice : DeviceBySpec<DemoDevice>(DemoDevice) {
Meta { Meta {
val time = Instant.now() val time = Instant.now()
"time" put time.toEpochMilli() "time" put time.toEpochMilli()
"x" put getSuspend(sin) "x" put read(sin)
"y" put getSuspend(cos) "y" put read(cos)
} }
} }
@ -46,13 +45,11 @@ class DemoDevice : DeviceBySpec<DemoDevice>(DemoDevice) {
null null
} }
@OptIn(ExperimentalTime::class)
override fun DemoDevice.onStartup() { override fun DemoDevice.onStartup() {
launch { doRecurring(Duration.milliseconds(50)){
while(isActive){ sin.read()
delay(50) cos.read()
sin.read()
cos.read()
}
} }
} }
} }