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>
/**
* 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)
}
}

View File

@ -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? =

View File

@ -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
)

View File

@ -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
)

View File

@ -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<D : DeviceBySpec<D>>(
public val spec: DeviceSpec<D>,
context: Context = Global,
@ -55,11 +58,9 @@ public open class DeviceBySpec<D : DeviceBySpec<D>>(
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<D : DeviceBySpec<D>>(
* 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<D, out Any>)?.let {
it.writeItem(self, value)
invalidateProperty(propertyName)
invalidate(propertyName)
} ?: run {
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 ->
if (oldValue != newValue) {
launch {
invalidateProperty(property.name)
invalidate(property.name)
_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() {
with(spec){ self.onShutdown() }
with(spec) { self.onShutdown() }
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>
): T = propertySpec.read(this@getSuspend).also {
updateLogical(propertySpec.name, propertySpec.converter.objectToMetaItem(it))
}
): T = propertySpec.read()
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)
}
public fun <D : DeviceBySpec<D>, T : Any> D.write(
propertySpec: WritableDevicePropertySpec<D, T>,
value: T
): Job = launch {
propertySpec.write(value)
}

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.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<in D : Device, T : Any> {
/**
@ -28,9 +35,11 @@ public interface DevicePropertySpec<in D : Device, T : Any> {
/**
* Read physical value from the given [device]
*/
@InternalDeviceAPI
public suspend fun read(device: D): T
}
@OptIn(InternalDeviceAPI::class)
public suspend fun <D : Device, T : Any> DevicePropertySpec<D, T>.readItem(device: D): MetaItem =
converter.objectToMetaItem(read(device))
@ -39,9 +48,11 @@ public interface WritableDevicePropertySpec<in D : Device, T : Any> : DeviceProp
/**
* Write physical value to a device
*/
@InternalDeviceAPI
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) {
write(device, converter.itemToObject(item))
}

View File

@ -13,6 +13,7 @@ import kotlin.reflect.KMutableProperty1
import kotlin.reflect.KProperty
import kotlin.reflect.KProperty1
@OptIn(InternalDeviceAPI::class)
public abstract class DeviceSpec<D : DeviceBySpec<D>>(
private val buildDevice: () -> 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
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 <D : DeviceBySpec<D>, T : Any> D.get(
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.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,

View File

@ -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>(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>(::DemoDevice) {
@ -34,8 +33,8 @@ class DemoDevice : DeviceBySpec<DemoDevice>(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>(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()
}
}
}