Basic design for property builders

This commit is contained in:
Alexander Nozik 2020-02-22 22:02:57 +03:00
parent 7ab80516e0
commit 69074fef9e
13 changed files with 407 additions and 13 deletions

View File

@ -10,7 +10,7 @@ kotlin {
sourceSets {
commonMain{
dependencies {
api("hep.dataforge:dataforge-meta:$dataforgeVersion")
api("hep.dataforge:dataforge-context:$dataforgeVersion")
}
}
}

View File

@ -1,13 +1,40 @@
package hep.dataforge.control.api
import hep.dataforge.meta.MetaItem
import kotlinx.coroutines.CoroutineScope
interface Device {
val descriptors: Collection<PropertyDescriptor>
var controller: DeviceController?
/**
* List of supported property descriptors
*/
val propertyDescriptors: Collection<PropertyDescriptor>
/**
* List of supported requests descriptors
*/
val requestDescriptors: Collection<RequestDescriptor>
/**
* The scope encompassing all operations on a device. When canceled, cancels all running processes
*/
val scope: CoroutineScope
var controller: PropertyChangeListener?
/**
* Get the value of the property or throw error if property in not defined. Suspend if property value is not available
*/
suspend fun getProperty(propertyName: String): MetaItem<*>
suspend fun setProperty(propertyName: String, propertyValue: MetaItem<*>)
suspend fun request(command: String, argument: MetaItem<*>?): MetaItem<*>?
/**
* 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.
*/
suspend fun setProperty(propertyName: String, value: MetaItem<*>)
/**
* Send a request and suspend caller while request is being processed.
* Could return null if request does not return meaningful answer.
*/
suspend fun request(name: String, argument: MetaItem<*>? = null): MetaItem<*>?
}

View File

@ -1,4 +0,0 @@
package hep.dataforge.control.api
interface DeviceController {
}

View File

@ -0,0 +1,23 @@
package hep.dataforge.control.api
import hep.dataforge.meta.MetaItem
/**
* A hub that could locate multiple devices and redirect actions to them
*/
interface DeviceHub {
fun getDevice(deviceName: String): Device?
}
suspend fun DeviceHub.getProperty(deviceName: String, propertyName: String): MetaItem<*> =
(getDevice(deviceName) ?: error("Device with name $deviceName not found in the hub"))
.getProperty(propertyName)
suspend fun DeviceHub.setProperty(deviceName: String, propertyName: String, value: MetaItem<*>) {
(getDevice(deviceName) ?: error("Device with name $deviceName not found in the hub"))
.setProperty(propertyName, value)
}
suspend fun DeviceHub.request(deviceName: String, command: String, argument: MetaItem<*>?): MetaItem<*>? =
(getDevice(deviceName) ?: error("Device with name $deviceName not found in the hub"))
.request(command, argument)

View File

@ -0,0 +1,7 @@
package hep.dataforge.control.api
import hep.dataforge.meta.MetaItem
interface PropertyChangeListener {
fun propertyChanged(propertyName: String, value: MetaItem<*>)
}

View File

@ -2,14 +2,13 @@ package hep.dataforge.control.api
import hep.dataforge.meta.scheme.Scheme
import hep.dataforge.meta.scheme.SchemeSpec
import hep.dataforge.meta.scheme.string
/**
* A descriptor for property
*/
class PropertyDescriptor: Scheme() {
var name by string{ error("Property name is mandatory")}
class PropertyDescriptor : Scheme() {
//var name by string { error("Property name is mandatory") }
//var descriptor by spec(ItemDescriptor)
companion object: SchemeSpec<PropertyDescriptor>(::PropertyDescriptor)
companion object : SchemeSpec<PropertyDescriptor>(::PropertyDescriptor)
}

View File

@ -0,0 +1,14 @@
package hep.dataforge.control.api
import hep.dataforge.meta.scheme.Scheme
import hep.dataforge.meta.scheme.SchemeSpec
/**
* A descriptor for property
*/
class RequestDescriptor : Scheme() {
//var name by string { error("Property name is mandatory") }
//var descriptor by spec(ItemDescriptor)
companion object : SchemeSpec<RequestDescriptor>(::RequestDescriptor)
}

View File

@ -0,0 +1,91 @@
package hep.dataforge.control.base
import hep.dataforge.control.api.Device
import hep.dataforge.control.api.PropertyChangeListener
import hep.dataforge.control.api.PropertyDescriptor
import hep.dataforge.control.api.RequestDescriptor
import hep.dataforge.meta.MetaItem
import kotlin.jvm.JvmStatic
import kotlin.reflect.KProperty
abstract class DeviceBase : Device, PropertyChangeListener {
private val properties = HashMap<String, ReadOnlyProperty>()
private val requests = HashMap<String, Request>()
override var controller: PropertyChangeListener? = null
override fun propertyChanged(propertyName: String, value: MetaItem<*>) {
controller?.propertyChanged(propertyName, value)
}
override val propertyDescriptors: Collection<PropertyDescriptor>
get() = properties.values.map { it.descriptor }
override val requestDescriptors: Collection<RequestDescriptor>
get() = requests.values.map { it.descriptor }
fun <P : ReadOnlyProperty> initProperty(prop: P): P {
properties[prop.name] = prop
return prop
}
fun initRequest(request: Request): Request {
requests[request.name] = request
return request
}
protected fun initRequest(
name: String,
descriptor: RequestDescriptor = RequestDescriptor.empty(),
block: suspend (MetaItem<*>?) -> MetaItem<*>?
): Request {
val request = SimpleRequest(name, descriptor, block)
return initRequest(request)
}
override suspend fun getProperty(propertyName: String): MetaItem<*> =
(properties[propertyName] ?: error("Property with name $propertyName not defined")).read()
override suspend fun setProperty(propertyName: String, value: MetaItem<*>) {
(properties[propertyName] as? Property ?: error("Property with name $propertyName not defined")).write(value)
}
override suspend fun request(name: String, argument: MetaItem<*>?): MetaItem<*>? =
(requests[name] ?: error("Request with name $name not defined")).invoke(argument)
companion object {
@JvmStatic
protected fun <D : DeviceBase, P : ReadOnlyProperty> D.initProperty(
name: String,
builder: PropertyBuilder<D>.() -> P
): P {
val property = PropertyBuilder(name, this).run(builder)
initProperty(property)
return property
}
}
}
class PropertyDelegateProvider<D : DeviceBase, T : Any, P : GenericReadOnlyProperty<D, T>>(
val owner: D,
val builder: PropertyBuilder<D>.() -> P
) {
operator fun provideDelegate(thisRef: D, property: KProperty<*>): P {
val name = property.name
return owner.initProperty(PropertyBuilder(name, owner).run(builder))
}
}
fun <D : DeviceBase, T : Any> D.property(
builder: PropertyBuilder<D>.() -> GenericReadOnlyProperty<D, T>
): PropertyDelegateProvider<D, T, GenericReadOnlyProperty<D, T>> {
return PropertyDelegateProvider(this, builder)
}
fun <D : DeviceBase, T : Any> D.mutableProperty(
builder: PropertyBuilder<D>.() -> GenericProperty<D, T>
): PropertyDelegateProvider<D, T, GenericProperty<D, T>> {
return PropertyDelegateProvider(this, builder)
}

View File

@ -0,0 +1,84 @@
package hep.dataforge.control.base
import hep.dataforge.control.api.PropertyDescriptor
import hep.dataforge.meta.MetaItem
import hep.dataforge.meta.transformations.MetaCaster
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty
open class GenericReadOnlyProperty<D: DeviceBase, T : Any>(
override val name: String,
override val descriptor: PropertyDescriptor,
override val owner: D,
internal val converter: MetaCaster<T>,
internal val getter: suspend D.() -> T
) : ReadOnlyProperty, kotlin.properties.ReadOnlyProperty<Any?, T?> {
protected val mutex = Mutex()
protected var value: T? = null
suspend fun updateValue(value: T) {
mutex.withLock { this.value = value }
owner.propertyChanged(name, converter.objectToMetaItem(value))
}
suspend fun readValue(): T =
value ?: withContext(owner.scope.coroutineContext) {
//all device operations should be run on device context
owner.getter().also { updateValue(it) }
}
fun peekValue(): T? = value
suspend fun update(item: MetaItem<*>) {
updateValue(converter.itemToObject(item))
}
override suspend fun read(): MetaItem<*> = converter.objectToMetaItem(readValue())
override fun peek(): MetaItem<*>? = value?.let { converter.objectToMetaItem(it) }
override fun getValue(thisRef: Any?, property: KProperty<*>): T? = peekValue()
}
class GenericProperty<D: DeviceBase, T : Any>(
name: String,
descriptor: PropertyDescriptor,
owner: D,
converter: MetaCaster<T>,
getter: suspend D.() -> T,
private val setter: suspend D.(oldValue: T?, newValue: T) -> Unit
) : Property, ReadWriteProperty<Any?, T?>, GenericReadOnlyProperty<D, T>(name, descriptor, owner, converter, getter) {
suspend fun writeValue(newValue: T) {
val oldValue = value
withContext(owner.scope.coroutineContext) {
//all device operations should be run on device context
invalidate()
owner.setter(oldValue, newValue)
}
}
override suspend fun invalidate() {
mutex.withLock { value = null }
}
override suspend fun write(item: MetaItem<*>) {
writeValue(converter.itemToObject(item))
}
override fun setValue(thisRef: Any?, property: KProperty<*>, value: T?) {
owner.scope.launch {
if (value == null) {
invalidate()
} else {
writeValue(value)
}
}
}
}

View File

@ -0,0 +1,54 @@
package hep.dataforge.control.base
import hep.dataforge.control.api.Device
import hep.dataforge.control.api.PropertyDescriptor
import hep.dataforge.meta.MetaItem
/**
* Read-only device property
*/
interface ReadOnlyProperty {
/**
* Property name, should be unique in device
*/
val name: String
val owner: Device
/**
* Property descriptor
*/
val descriptor: PropertyDescriptor
/**
* Get cached value and return null if value is invalid
*/
fun peek(): MetaItem<*>?
/**
* Read value either from cache if cache is valid or directly from physical device
*/
suspend fun read(): MetaItem<*>
}
/**
* A single writeable property handler
*/
interface Property : ReadOnlyProperty {
/**
* Update property logical value and notify listener without writing it to device
*/
suspend fun update(item: MetaItem<*>)
/**
* Erase logical value and force re-read from device on next [read]
*/
suspend fun invalidate()
/**
* Write value to physical device. Invalidates logical value, but does not update it automatically
*/
suspend fun write(item: MetaItem<*>)
}

View File

@ -0,0 +1,46 @@
package hep.dataforge.control.base
import hep.dataforge.control.api.PropertyDescriptor
import hep.dataforge.meta.transformations.MetaCaster
import hep.dataforge.values.Value
class PropertyBuilder<D : DeviceBase>(val name: String, val owner: D) {
var descriptor: PropertyDescriptor = PropertyDescriptor.empty()
inline fun descriptor(block: PropertyDescriptor.() -> Unit) {
descriptor.apply(block)
}
fun <T : Any> get(converter: MetaCaster<T>, getter: (suspend D.() -> T)): GenericReadOnlyProperty<D, T> =
GenericReadOnlyProperty(name, descriptor, owner, converter, getter)
fun getDouble(getter: (suspend D.() -> Double)): GenericReadOnlyProperty<D, Double> =
GenericReadOnlyProperty(name, descriptor, owner, MetaCaster.double) { getter() }
fun getString(getter: suspend D.() -> String): GenericReadOnlyProperty<D, String> =
GenericReadOnlyProperty(name, descriptor, owner, MetaCaster.string) { getter() }
fun getBoolean(getter: suspend D.() -> Boolean): GenericReadOnlyProperty<D, Boolean> =
GenericReadOnlyProperty(name, descriptor, owner, MetaCaster.boolean) { getter() }
fun getValue(getter: suspend D.() -> Any?): GenericReadOnlyProperty<D, Value> =
GenericReadOnlyProperty(name, descriptor, owner, MetaCaster.value) { Value.of(getter()) }
/**
* Convert this read-only property to read-write property
*/
infix fun <T: Any> GenericReadOnlyProperty<D, T>.set(setter: (suspend D.(oldValue: T?, newValue: T) -> Unit)): GenericProperty<D,T> {
return GenericProperty(name, descriptor, owner, converter, getter, setter)
}
/**
* Create read-write property with synchronized setter which updates value after set
*/
fun <T: Any> GenericReadOnlyProperty<D, T>.set(synchronousSetter: (suspend D.(oldValue: T?, newValue: T) -> T)): GenericProperty<D,T> {
val setter: suspend D.(oldValue: T?, newValue: T) -> Unit = { oldValue, newValue ->
val result = synchronousSetter(oldValue, newValue)
updateValue(result)
}
return GenericProperty(name, descriptor, owner, converter, getter, setter)
}
}

View File

@ -0,0 +1,18 @@
package hep.dataforge.control.base
import hep.dataforge.control.api.RequestDescriptor
import hep.dataforge.meta.MetaItem
interface Request {
val name: String
val descriptor: RequestDescriptor
suspend operator fun invoke(arg: MetaItem<*>?): MetaItem<*>?
}
class SimpleRequest(
override val name: String,
override val descriptor: RequestDescriptor,
val block: suspend (MetaItem<*>?)->MetaItem<*>?
): Request{
override suspend fun invoke(arg: MetaItem<*>?): MetaItem<*>? = block(arg)
}

View File

@ -0,0 +1,35 @@
package hep.dataforge.control.demo
import hep.dataforge.control.base.DeviceBase
import hep.dataforge.control.base.mutableProperty
import hep.dataforge.control.base.property
import hep.dataforge.meta.Meta
import kotlinx.coroutines.CoroutineScope
import java.time.Instant
import kotlin.math.cos
import kotlin.math.sin
class VirtualDevice(val meta: Meta, override val scope: CoroutineScope) : DeviceBase() {
var scale by mutableProperty {
getDouble {
200.0
} set { _, _ ->
}
}
val sin by property {
getDouble {
val time = Instant.now()
sin(time.toEpochMilli().toDouble() / (scale ?: 1000.0))
}
}
val cos by property {
getDouble {
val time = Instant.now()
cos(time.toEpochMilli().toDouble() / (scale ?: 1000.0))
}
}
}