Add port demo. Nullable properties

This commit is contained in:
Alexander Nozik 2023-03-01 20:59:18 +03:00
parent 5b16b07172
commit ba84553be5
26 changed files with 431 additions and 94 deletions

View File

@ -57,7 +57,7 @@ public interface Device : Closeable, ContextAware, CoroutineScope {
/** /**
* 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 writeProperty(propertyName: String, value: Meta) public suspend fun writeProperty(propertyName: String, value: Meta)
@ -101,7 +101,7 @@ public suspend fun Device.getOrReadProperty(propertyName: String): Meta =
* *
* TODO currently this * TODO currently this
*/ */
public fun Device.getProperties(): Meta = Meta { public fun Device.getAllProperties(): Meta = Meta {
for (descriptor in propertyDescriptors) { for (descriptor in propertyDescriptors) {
setMeta(Name.parse(descriptor.name), getProperty(descriptor.name)) setMeta(Name.parse(descriptor.name), getProperty(descriptor.name))
} }

View File

@ -7,7 +7,7 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
/** /**
* A generic bi-directional sender/receiver object * A generic bidirectional sender/receiver object
*/ */
public interface Socket<T> : Closeable { public interface Socket<T> : Closeable {
/** /**

View File

@ -6,11 +6,19 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.flow.receiveAsFlow
import space.kscience.controls.api.Socket import space.kscience.controls.api.Socket
import space.kscience.dataforge.context.* import space.kscience.dataforge.context.*
import space.kscience.dataforge.misc.Type
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
public interface Port : ContextAware, Socket<ByteArray> public interface Port : ContextAware, Socket<ByteArray>
public typealias PortFactory = Factory<Port> @Type(PortFactory.TYPE)
public interface PortFactory: Factory<Port>{
public val type: String
public companion object{
public const val TYPE: String = "controls.port"
}
}
public abstract class AbstractPort( public abstract class AbstractPort(
override val context: Context, override val context: Context,
@ -64,12 +72,10 @@ public abstract class AbstractPort(
/** /**
* Raw flow of incoming data chunks. The chunks are not guaranteed to be complete phrases. * Raw flow of incoming data chunks. The chunks are not guaranteed to be complete phrases.
* In order to form phrases some condition should used on top of it. * In order to form phrases some condition should be used on top of it.
* For example [delimitedIncoming] generates phrases with fixed delimiter. * For example [delimitedIncoming] generates phrases with fixed delimiter.
*/ */
override fun receiving(): Flow<ByteArray> { override fun receiving(): Flow<ByteArray> = incoming.receiveAsFlow()
return incoming.receiveAsFlow()
}
override fun close() { override fun close() {
outgoing.close() outgoing.close()

View File

@ -0,0 +1,34 @@
package space.kscience.controls.ports
import space.kscience.dataforge.context.*
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.string
import kotlin.reflect.KClass
public class Ports : AbstractPlugin() {
override val tag: PluginTag get() = Companion.tag
private val portFactories by lazy {
context.gather<PortFactory>(PortFactory.TYPE)
}
private val portCache = mutableMapOf<Meta, Port>()
public fun buildPort(meta: Meta): Port = portCache.getOrPut(meta) {
val type by meta.string { error("Port type is not defined") }
val factory = portFactories.values.firstOrNull { it.type == type }
?: error("Port factory for type $type not found")
factory.build(context, meta)
}
public companion object : PluginFactory<Ports> {
override val tag: PluginTag = PluginTag("controls.ports", group = PluginTag.DATAFORGE_GROUP)
override val type: KClass<out Ports> = Ports::class
override fun build(context: Context, meta: Meta): Ports = Ports()
}
}

View File

@ -8,27 +8,35 @@ import kotlinx.coroutines.sync.withLock
/** /**
* A port handler for synchronous (request-response) communication with a port. Only one request could be active at a time (others are suspended. * A port handler for synchronous (request-response) communication with a port. Only one request could be active at a time (others are suspended.
* The handler does not guarantee exclusive access to the port so the user mush ensure that no other controller handles port at the moment. * The handler does not guarantee exclusive access to the port so the user mush ensure that no other controller handles port at the moment.
*
*/ */
public class SynchronousPortHandler(public val port: Port) { public class SynchronousPort(public val port: Port, private val mutex: Mutex) : Port by port {
private val mutex = Mutex()
/** /**
* Send a single message and wait for the flow of respond messages. * Send a single message and wait for the flow of respond messages.
*/ */
public suspend fun <R> respond(data: ByteArray, transform: suspend Flow<ByteArray>.() -> R): R { public suspend fun <R> respond(data: ByteArray, transform: suspend Flow<ByteArray>.() -> R): R = mutex.withLock {
return mutex.withLock { port.send(data)
port.send(data) transform(port.receiving())
transform(port.receiving())
}
} }
} }
/**
* Provide a synchronous wrapper for a port
*/
public fun Port.synchronous(mutex: Mutex = Mutex()): SynchronousPort = SynchronousPort(this, mutex)
/** /**
* Send request and read incoming data blocks until the delimiter is encountered * Send request and read incoming data blocks until the delimiter is encountered
*/ */
public suspend fun SynchronousPortHandler.respondWithDelimiter(data: ByteArray, delimiter: ByteArray): ByteArray { public suspend fun SynchronousPort.respondWithDelimiter(
return respond(data) { data: ByteArray,
withDelimiter(delimiter).first() delimiter: ByteArray,
} ): ByteArray = respond(data) {
withDelimiter(delimiter).first()
}
public suspend fun SynchronousPort.respondStringWithDelimiter(
data: String,
delimiter: String,
): String = respond(data.encodeToByteArray()) {
withStringDelimiter(delimiter).first()
} }

View File

@ -40,12 +40,11 @@ public fun Flow<ByteArray>.withDelimiter(delimiter: ByteArray): Flow<ByteArray>
/** /**
* Transform byte fragments into utf-8 phrases using utf-8 delimiter * Transform byte fragments into utf-8 phrases using utf-8 delimiter
*/ */
public fun Flow<ByteArray>.withDelimiter(delimiter: String): Flow<String> { public fun Flow<ByteArray>.withStringDelimiter(delimiter: String): Flow<String> {
return withDelimiter(delimiter.encodeToByteArray()).map { it.decodeToString() } return withDelimiter(delimiter.encodeToByteArray()).map { it.decodeToString() }
} }
/** /**
* A flow of delimited phrases * A flow of delimited phrases
*/ */
public suspend fun Port.delimitedIncoming(delimiter: ByteArray): Flow<ByteArray> = public fun Port.delimitedIncoming(delimiter: ByteArray): Flow<ByteArray> = receiving().withDelimiter(delimiter)
receiving().withDelimiter(delimiter)

View File

@ -17,10 +17,17 @@ import kotlin.coroutines.CoroutineContext
@OptIn(InternalDeviceAPI::class) @OptIn(InternalDeviceAPI::class)
public abstract class DeviceBase<D : DeviceBase<D>>( public abstract class DeviceBase<D : DeviceBase<D>>(
override val context: Context = Global, override val context: Context = Global,
override val meta: Meta = Meta.EMPTY override val meta: Meta = Meta.EMPTY,
) : Device { ) : Device {
/**
* Collection of property specifications
*/
public abstract val properties: Map<String, DevicePropertySpec<D, *>> public abstract val properties: Map<String, DevicePropertySpec<D, *>>
/**
* Collection of action specifications
*/
public abstract val actions: Map<String, DeviceActionSpec<D, *, *>> public abstract val actions: Map<String, DeviceActionSpec<D, *, *>>
override val propertyDescriptors: Collection<PropertyDescriptor> override val propertyDescriptors: Collection<PropertyDescriptor>
@ -33,6 +40,9 @@ public abstract class DeviceBase<D : DeviceBase<D>>(
context.coroutineContext + SupervisorJob(context.coroutineContext[Job]) context.coroutineContext + SupervisorJob(context.coroutineContext[Job])
} }
/**
* Logical state store
*/
private val logicalState: HashMap<String, Meta?> = HashMap() private val logicalState: HashMap<String, Meta?> = HashMap()
private val sharedMessageFlow: MutableSharedFlow<DeviceMessage> = MutableSharedFlow() private val sharedMessageFlow: MutableSharedFlow<DeviceMessage> = MutableSharedFlow()
@ -99,14 +109,18 @@ public abstract class DeviceBase<D : DeviceBase<D>>(
actions[action]?.executeWithMeta(self, argument) actions[action]?.executeWithMeta(self, argument)
/** /**
* Read typed value and update/push event if needed * Read typed value and update/push event if needed.
* Return null if property read is not successful or property is undefined.
*/ */
public suspend fun <T> DevicePropertySpec<D, T>.read(): T { public suspend fun <T> DevicePropertySpec<D, T>.readOrNull(): T? {
val res = read(self) val res = read(self) ?: return null
updateLogical(name, converter.objectToMeta(res)) updateLogical(name, converter.objectToMeta(res))
return res return res
} }
public suspend fun <T> DevicePropertySpec<D, T>.read(): T =
readOrNull() ?: error("Failed to read property $name state")
public fun <T> DevicePropertySpec<D, T>.get(): T? = getProperty(name)?.let(converter::metaToObject) public fun <T> DevicePropertySpec<D, T>.get(): T? = getProperty(name)?.let(converter::metaToObject)
/** /**
@ -121,6 +135,13 @@ public abstract class DeviceBase<D : DeviceBase<D>>(
} }
} }
/**
* Reset logical state of a property
*/
public suspend fun DevicePropertySpec<D, *>.invalidate() {
invalidate(name)
}
public suspend operator fun <I, O> DeviceActionSpec<D, I, O>.invoke(input: I? = null): O? = execute(self, input) public suspend operator fun <I, O> DeviceActionSpec<D, I, O>.invoke(input: I? = null): O? = execute(self, input)
} }
@ -132,17 +153,17 @@ public abstract class DeviceBase<D : DeviceBase<D>>(
public open class DeviceBySpec<D : DeviceBySpec<D>>( public open class DeviceBySpec<D : DeviceBySpec<D>>(
public val spec: DeviceSpec<in D>, public val spec: DeviceSpec<in D>,
context: Context = Global, context: Context = Global,
meta: Meta = Meta.EMPTY meta: Meta = Meta.EMPTY,
) : DeviceBase<D>(context, meta) { ) : DeviceBase<D>(context, meta) {
override val properties: Map<String, DevicePropertySpec<D, *>> get() = spec.properties override val properties: Map<String, DevicePropertySpec<D, *>> get() = spec.properties
override val actions: Map<String, DeviceActionSpec<D, *, *>> get() = spec.actions override val actions: Map<String, DeviceActionSpec<D, *, *>> get() = spec.actions
override suspend fun open(): Unit = with(spec){ override suspend fun open(): Unit = with(spec) {
super.open() super.open()
self.onOpen() self.onOpen()
} }
override fun close(): Unit = with(spec){ override fun close(): Unit = with(spec) {
self.onClose() self.onClose()
super.close() super.close()
} }

View File

@ -35,7 +35,7 @@ public interface DevicePropertySpec<in D : Device, T> {
* Read physical value from the given [device] * Read physical value from the given [device]
*/ */
@InternalDeviceAPI @InternalDeviceAPI
public suspend fun read(device: D): T public suspend fun read(device: D): T?
} }
/** /**
@ -44,8 +44,8 @@ public interface DevicePropertySpec<in D : Device, T> {
public val DevicePropertySpec<*, *>.name: String get() = descriptor.name public val DevicePropertySpec<*, *>.name: String get() = descriptor.name
@OptIn(InternalDeviceAPI::class) @OptIn(InternalDeviceAPI::class)
public suspend fun <D : Device, T> DevicePropertySpec<D, T>.readMeta(device: D): Meta = public suspend fun <D : Device, T> DevicePropertySpec<D, T>.readMeta(device: D): Meta? =
converter.objectToMeta(read(device)) read(device)?.let(converter::objectToMeta)
public interface WritableDevicePropertySpec<in D : Device, T> : DevicePropertySpec<D, T> { public interface WritableDevicePropertySpec<in D : Device, T> : DevicePropertySpec<D, T> {

View File

@ -24,25 +24,26 @@ public abstract class DeviceSpec<D : Device> {
public val actions: Map<String, DeviceActionSpec<D, *, *>> get() = _actions public val actions: Map<String, DeviceActionSpec<D, *, *>> get() = _actions
public open suspend fun D.onOpen(){ public open suspend fun D.onOpen() {
} }
public open fun D.onClose(){ public open fun D.onClose() {
} }
public fun <T : Any, P : DevicePropertySpec<D, T>> registerProperty(deviceProperty: P): P { public fun <T, P : DevicePropertySpec<D, T>> registerProperty(deviceProperty: P): P {
_properties[deviceProperty.name] = deviceProperty _properties[deviceProperty.name] = deviceProperty
return deviceProperty return deviceProperty
} }
public fun <T : Any> registerProperty( public fun <T> registerProperty(
converter: MetaConverter<T>, converter: MetaConverter<T>,
readOnlyProperty: KProperty1<D, T>, readOnlyProperty: KProperty1<D, T>,
descriptorBuilder: PropertyDescriptor.() -> Unit = {} descriptorBuilder: PropertyDescriptor.() -> Unit = {},
): DevicePropertySpec<D, T> { ): DevicePropertySpec<D, T> {
val deviceProperty = object : DevicePropertySpec<D, T> { val deviceProperty = object : DevicePropertySpec<D, T> {
override val descriptor: PropertyDescriptor = PropertyDescriptor(readOnlyProperty.name).apply(descriptorBuilder) override val descriptor: PropertyDescriptor =
PropertyDescriptor(readOnlyProperty.name).apply(descriptorBuilder)
override val converter: MetaConverter<T> = converter override val converter: MetaConverter<T> = converter
override suspend fun read(device: D): T = override suspend fun read(device: D): T =
withContext(device.coroutineContext) { readOnlyProperty.get(device) } withContext(device.coroutineContext) { readOnlyProperty.get(device) }
@ -50,11 +51,11 @@ public abstract class DeviceSpec<D : Device> {
return registerProperty(deviceProperty) return registerProperty(deviceProperty)
} }
public fun <T : Any> property( public fun <T> property(
converter: MetaConverter<T>, converter: MetaConverter<T>,
readOnlyProperty: KProperty1<D, T>, readOnlyProperty: KProperty1<D, T>,
descriptorBuilder: PropertyDescriptor.() -> Unit = {} descriptorBuilder: PropertyDescriptor.() -> Unit = {},
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<Any?,DevicePropertySpec<D, T>>> = ): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<Any?, DevicePropertySpec<D, T>>> =
PropertyDelegateProvider { _, property -> PropertyDelegateProvider { _, property ->
val deviceProperty = object : DevicePropertySpec<D, T> { val deviceProperty = object : DevicePropertySpec<D, T> {
override val descriptor: PropertyDescriptor = PropertyDescriptor(property.name).apply { override val descriptor: PropertyDescriptor = PropertyDescriptor(property.name).apply {
@ -74,10 +75,10 @@ public abstract class DeviceSpec<D : Device> {
} }
} }
public fun <T : Any> mutableProperty( public fun <T> mutableProperty(
converter: MetaConverter<T>, converter: MetaConverter<T>,
readWriteProperty: KMutableProperty1<D, T>, readWriteProperty: KMutableProperty1<D, T>,
descriptorBuilder: PropertyDescriptor.() -> Unit = {} descriptorBuilder: PropertyDescriptor.() -> Unit = {},
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<Any?, WritableDevicePropertySpec<D, T>>> = ): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<Any?, WritableDevicePropertySpec<D, T>>> =
PropertyDelegateProvider { _, property -> PropertyDelegateProvider { _, property ->
val deviceProperty = object : WritableDevicePropertySpec<D, T> { val deviceProperty = object : WritableDevicePropertySpec<D, T> {
@ -103,11 +104,11 @@ public abstract class DeviceSpec<D : Device> {
} }
} }
public fun <T : Any> property( public fun <T> property(
converter: MetaConverter<T>, converter: MetaConverter<T>,
descriptorBuilder: PropertyDescriptor.() -> Unit = {}, descriptorBuilder: PropertyDescriptor.() -> Unit = {},
name: String? = null, name: String? = null,
read: suspend D.() -> T read: suspend D.() -> T?,
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DevicePropertySpec<D, T>>> = ): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DevicePropertySpec<D, T>>> =
PropertyDelegateProvider { _: DeviceSpec<D>, property -> PropertyDelegateProvider { _: DeviceSpec<D>, property ->
val propertyName = name ?: property.name val propertyName = name ?: property.name
@ -115,7 +116,7 @@ public abstract class DeviceSpec<D : Device> {
override val descriptor: PropertyDescriptor = PropertyDescriptor(propertyName).apply(descriptorBuilder) override val descriptor: PropertyDescriptor = PropertyDescriptor(propertyName).apply(descriptorBuilder)
override val converter: MetaConverter<T> = converter override val converter: MetaConverter<T> = converter
override suspend fun read(device: D): T = withContext(device.coroutineContext) { device.read() } override suspend fun read(device: D): T? = withContext(device.coroutineContext) { device.read() }
} }
registerProperty(deviceProperty) registerProperty(deviceProperty)
ReadOnlyProperty<DeviceSpec<D>, DevicePropertySpec<D, T>> { _, _ -> ReadOnlyProperty<DeviceSpec<D>, DevicePropertySpec<D, T>> { _, _ ->
@ -123,12 +124,12 @@ public abstract class DeviceSpec<D : Device> {
} }
} }
public fun <T : Any> mutableProperty( public fun <T> mutableProperty(
converter: MetaConverter<T>, converter: MetaConverter<T>,
descriptorBuilder: PropertyDescriptor.() -> Unit = {}, descriptorBuilder: PropertyDescriptor.() -> Unit = {},
name: String? = null, name: String? = null,
read: suspend D.() -> T, read: suspend D.() -> T?,
write: suspend D.(T) -> Unit write: suspend D.(T) -> Unit,
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, WritableDevicePropertySpec<D, T>>> = ): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, WritableDevicePropertySpec<D, T>>> =
PropertyDelegateProvider { _: DeviceSpec<D>, property: KProperty<*> -> PropertyDelegateProvider { _: DeviceSpec<D>, property: KProperty<*> ->
val propertyName = name ?: property.name val propertyName = name ?: property.name
@ -136,7 +137,7 @@ public abstract class DeviceSpec<D : Device> {
override val descriptor: PropertyDescriptor = PropertyDescriptor(propertyName).apply(descriptorBuilder) override val descriptor: PropertyDescriptor = PropertyDescriptor(propertyName).apply(descriptorBuilder)
override val converter: MetaConverter<T> = converter override val converter: MetaConverter<T> = converter
override suspend fun read(device: D): T = withContext(device.coroutineContext) { device.read() } override suspend fun read(device: D): T? = withContext(device.coroutineContext) { device.read() }
override suspend fun write(device: D, value: T): Unit = withContext(device.coroutineContext) { override suspend fun write(device: D, value: T): Unit = withContext(device.coroutineContext) {
device.write(value) device.write(value)
@ -149,17 +150,17 @@ public abstract class DeviceSpec<D : Device> {
} }
public fun <I : Any, O : Any> registerAction(deviceAction: DeviceActionSpec<D, I, O>): DeviceActionSpec<D, I, O> { public fun <I, O> registerAction(deviceAction: DeviceActionSpec<D, I, O>): DeviceActionSpec<D, I, O> {
_actions[deviceAction.name] = deviceAction _actions[deviceAction.name] = deviceAction
return deviceAction return deviceAction
} }
public fun <I : Any, O : Any> action( public fun <I, O> action(
inputConverter: MetaConverter<I>, inputConverter: MetaConverter<I>,
outputConverter: MetaConverter<O>, outputConverter: MetaConverter<O>,
descriptorBuilder: ActionDescriptor.() -> Unit = {}, descriptorBuilder: ActionDescriptor.() -> Unit = {},
name: String? = null, name: String? = null,
execute: suspend D.(I?) -> O? execute: suspend D.(I?) -> O?,
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DeviceActionSpec<D, I, O>>> = ): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DeviceActionSpec<D, I, O>>> =
PropertyDelegateProvider { _: DeviceSpec<D>, property -> PropertyDelegateProvider { _: DeviceSpec<D>, property ->
val actionName = name ?: property.name val actionName = name ?: property.name
@ -185,15 +186,16 @@ public abstract class DeviceSpec<D : Device> {
public fun metaAction( public fun metaAction(
descriptorBuilder: ActionDescriptor.() -> Unit = {}, descriptorBuilder: ActionDescriptor.() -> Unit = {},
name: String? = null, name: String? = null,
execute: suspend D.(Meta?) -> Meta? execute: suspend D.(Meta?) -> Meta?,
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DeviceActionSpec<D, Meta, Meta>>> = action( ): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DeviceActionSpec<D, Meta, Meta>>> =
MetaConverter.Companion.meta, action(
MetaConverter.Companion.meta, MetaConverter.Companion.meta,
descriptorBuilder, MetaConverter.Companion.meta,
name descriptorBuilder,
){ name
execute(it) ) {
} execute(it)
}
/** /**
* An action that takes no parameters and returns no values * An action that takes no parameters and returns no values
@ -201,14 +203,47 @@ public abstract class DeviceSpec<D : Device> {
public fun unitAction( public fun unitAction(
descriptorBuilder: ActionDescriptor.() -> Unit = {}, descriptorBuilder: ActionDescriptor.() -> Unit = {},
name: String? = null, name: String? = null,
execute: suspend D.() -> Unit execute: suspend D.() -> Unit,
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DeviceActionSpec<D, Meta, Meta>>> = action( ): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DeviceActionSpec<D, Meta, Meta>>> =
MetaConverter.Companion.meta, action(
MetaConverter.Companion.meta, MetaConverter.Companion.meta,
descriptorBuilder, MetaConverter.Companion.meta,
name descriptorBuilder,
){ name
execute() ) {
null execute()
} null
}
} }
/**
* Register a mutable logical property for a device
*/
@OptIn(InternalDeviceAPI::class)
public fun <T, D : DeviceBase<D>> DeviceSpec<D>.logicalProperty(
converter: MetaConverter<T>,
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
name: String? = null,
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<Any?, WritableDevicePropertySpec<D, T>>> =
PropertyDelegateProvider { _, property ->
val deviceProperty = object : WritableDevicePropertySpec<D, T> {
val propertyName = name ?: property.name
override val descriptor: PropertyDescriptor = PropertyDescriptor(propertyName).apply {
//TODO add type from converter
writable = true
}.apply(descriptorBuilder)
override val converter: MetaConverter<T> = converter
override suspend fun read(device: D): T? =
device.getProperty(propertyName)?.let(converter::metaToObject)
override suspend fun write(device: D, value: T): Unit =
device.writeProperty(propertyName, converter.objectToMeta(value))
}
registerProperty(deviceProperty)
ReadOnlyProperty { _, _ ->
deviceProperty
}
}

View File

@ -13,7 +13,7 @@ import kotlin.properties.ReadOnlyProperty
public fun <D : DeviceBase<D>> DeviceSpec<D>.booleanProperty( public fun <D : DeviceBase<D>> DeviceSpec<D>.booleanProperty(
descriptorBuilder: PropertyDescriptor.() -> Unit = {}, descriptorBuilder: PropertyDescriptor.() -> Unit = {},
name: String? = null, name: String? = null,
read: suspend D.() -> Boolean read: suspend D.() -> Boolean?
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DevicePropertySpec<D, Boolean>>> = property( ): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DevicePropertySpec<D, Boolean>>> = property(
MetaConverter.boolean, MetaConverter.boolean,
{ {
@ -38,7 +38,7 @@ private inline fun numberDescriptor(
public fun <D : DeviceBase<D>> DeviceSpec<D>.numberProperty( public fun <D : DeviceBase<D>> DeviceSpec<D>.numberProperty(
name: String? = null, name: String? = null,
descriptorBuilder: PropertyDescriptor.() -> Unit = {}, descriptorBuilder: PropertyDescriptor.() -> Unit = {},
read: suspend D.() -> Number read: suspend D.() -> Number?
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DevicePropertySpec<D, Number>>> = property( ): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DevicePropertySpec<D, Number>>> = property(
MetaConverter.number, MetaConverter.number,
numberDescriptor(descriptorBuilder), numberDescriptor(descriptorBuilder),
@ -49,7 +49,7 @@ public fun <D : DeviceBase<D>> DeviceSpec<D>.numberProperty(
public fun <D : DeviceBase<D>> DeviceSpec<D>.doubleProperty( public fun <D : DeviceBase<D>> DeviceSpec<D>.doubleProperty(
descriptorBuilder: PropertyDescriptor.() -> Unit = {}, descriptorBuilder: PropertyDescriptor.() -> Unit = {},
name: String? = null, name: String? = null,
read: suspend D.() -> Double read: suspend D.() -> Double?
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DevicePropertySpec<D, Double>>> = property( ): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DevicePropertySpec<D, Double>>> = property(
MetaConverter.double, MetaConverter.double,
numberDescriptor(descriptorBuilder), numberDescriptor(descriptorBuilder),
@ -60,7 +60,7 @@ public fun <D : DeviceBase<D>> DeviceSpec<D>.doubleProperty(
public fun <D : DeviceBase<D>> DeviceSpec<D>.stringProperty( public fun <D : DeviceBase<D>> DeviceSpec<D>.stringProperty(
descriptorBuilder: PropertyDescriptor.() -> Unit = {}, descriptorBuilder: PropertyDescriptor.() -> Unit = {},
name: String? = null, name: String? = null,
read: suspend D.() -> String read: suspend D.() -> String?
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DevicePropertySpec<D, String>>> = property( ): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DevicePropertySpec<D, String>>> = property(
MetaConverter.string, MetaConverter.string,
{ {
@ -76,7 +76,7 @@ public fun <D : DeviceBase<D>> DeviceSpec<D>.stringProperty(
public fun <D : DeviceBase<D>> DeviceSpec<D>.metaProperty( public fun <D : DeviceBase<D>> DeviceSpec<D>.metaProperty(
descriptorBuilder: PropertyDescriptor.() -> Unit = {}, descriptorBuilder: PropertyDescriptor.() -> Unit = {},
name: String? = null, name: String? = null,
read: suspend D.() -> Meta read: suspend D.() -> Meta?
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DevicePropertySpec<D, Meta>>> = property( ): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DevicePropertySpec<D, Meta>>> = property(
MetaConverter.meta, MetaConverter.meta,
{ {
@ -94,7 +94,7 @@ public fun <D : DeviceBase<D>> DeviceSpec<D>.metaProperty(
public fun <D : DeviceBase<D>> DeviceSpec<D>.booleanProperty( public fun <D : DeviceBase<D>> DeviceSpec<D>.booleanProperty(
descriptorBuilder: PropertyDescriptor.() -> Unit = {}, descriptorBuilder: PropertyDescriptor.() -> Unit = {},
name: String? = null, name: String? = null,
read: suspend D.() -> Boolean, read: suspend D.() -> Boolean?,
write: suspend D.(Boolean) -> Unit write: suspend D.(Boolean) -> Unit
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, WritableDevicePropertySpec<D, Boolean>>> = ): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, WritableDevicePropertySpec<D, Boolean>>> =
mutableProperty( mutableProperty(

View File

@ -52,20 +52,20 @@ public class TcpPort private constructor(
} }
if (num < 0) cancel("The input channel is exhausted") if (num < 0) cancel("The input channel is exhausted")
} catch (ex: Exception) { } catch (ex: Exception) {
logger.error(ex){"Channel read error"} logger.error(ex) { "Channel read error" }
delay(1000) delay(1000)
} }
} }
} }
override suspend fun write(data: ByteArray) { override suspend fun write(data: ByteArray): Unit = withContext(Dispatchers.IO){
futureChannel.await().write(ByteBuffer.wrap(data)) futureChannel.await().write(ByteBuffer.wrap(data))
} }
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
override fun close() { override fun close() {
listenerJob.cancel() listenerJob.cancel()
if(futureChannel.isCompleted){ if (futureChannel.isCompleted) {
futureChannel.getCompleted().close() futureChannel.getCompleted().close()
} else { } else {
futureChannel.cancel() futureChannel.cancel()
@ -74,6 +74,9 @@ public class TcpPort private constructor(
} }
public companion object : PortFactory { public companion object : PortFactory {
override val type: String = "tcp"
public fun open( public fun open(
context: Context, context: Context,
host: String, host: String,

View File

@ -0,0 +1,30 @@
package space.kscience.controls.ports
import space.kscience.dataforge.context.AbstractPlugin
import space.kscience.dataforge.context.Context
import space.kscience.dataforge.context.PluginFactory
import space.kscience.dataforge.context.PluginTag
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.names.Name
import kotlin.reflect.KClass
public class TcpPortPlugin : AbstractPlugin() {
override val tag: PluginTag get() = Companion.tag
override fun content(target: String): Map<Name, Any> = when(target){
PortFactory.TYPE -> mapOf(Name.EMPTY to TcpPort)
else -> emptyMap()
}
public companion object : PluginFactory<TcpPortPlugin> {
override val tag: PluginTag = PluginTag("controls.ports.tcp", group = PluginTag.DATAFORGE_GROUP)
override val type: KClass<out TcpPortPlugin> = TcpPortPlugin::class
override fun build(context: Context, meta: Meta): TcpPortPlugin = TcpPortPlugin()
}
}

View File

@ -7,4 +7,4 @@ import kotlinx.coroutines.runBlocking
*/ */
public operator fun <D : DeviceBase<D>, T : Any> D.get( public operator fun <D : DeviceBase<D>, T : Any> D.get(
propertySpec: DevicePropertySpec<D, T> propertySpec: DevicePropertySpec<D, T>
): T = runBlocking { read(propertySpec) } ): T? = runBlocking { read(propertySpec) }

View File

@ -5,6 +5,6 @@ plugins {
val ktorVersion: String by rootProject.extra val ktorVersion: String by rootProject.extra
dependencies { dependencies {
api(project(":controls-core")) api(projects.controlsCore)
api("io.ktor:ktor-network:$ktorVersion") api("io.ktor:ktor-network:$ktorVersion")
} }

View File

@ -55,7 +55,10 @@ public class KtorTcpPort internal constructor(
super.close() super.close()
} }
public companion object: PortFactory { public companion object : PortFactory {
override val type: String = "tcp"
public fun open( public fun open(
context: Context, context: Context,
host: String, host: String,

View File

@ -0,0 +1,30 @@
package space.kscience.controls.ports
import space.kscience.dataforge.context.AbstractPlugin
import space.kscience.dataforge.context.Context
import space.kscience.dataforge.context.PluginFactory
import space.kscience.dataforge.context.PluginTag
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.names.Name
import kotlin.reflect.KClass
public class KtorTcpPortPlugin : AbstractPlugin() {
override val tag: PluginTag get() = Companion.tag
override fun content(target: String): Map<Name, Any> = when(target){
PortFactory.TYPE -> mapOf(Name.EMPTY to KtorTcpPort)
else -> emptyMap()
}
public companion object : PluginFactory<KtorTcpPortPlugin> {
override val tag: PluginTag = PluginTag("controls.ports.serial", group = PluginTag.DATAFORGE_GROUP)
override val type: KClass<out KtorTcpPortPlugin> = KtorTcpPortPlugin::class
override fun build(context: Context, meta: Meta): KtorTcpPortPlugin = KtorTcpPortPlugin()
}
}

View File

@ -58,6 +58,9 @@ public class SerialPort private constructor(
public companion object : PortFactory { public companion object : PortFactory {
override val type: String = "com"
/** /**
* Construct ComPort with given parameters * Construct ComPort with given parameters
*/ */

View File

@ -0,0 +1,31 @@
package space.kscience.controls.serial
import space.kscience.controls.ports.PortFactory
import space.kscience.dataforge.context.AbstractPlugin
import space.kscience.dataforge.context.Context
import space.kscience.dataforge.context.PluginFactory
import space.kscience.dataforge.context.PluginTag
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.names.Name
import kotlin.reflect.KClass
public class SerialPortPlugin : AbstractPlugin() {
override val tag: PluginTag get() = Companion.tag
override fun content(target: String): Map<Name, Any> = when(target){
PortFactory.TYPE -> mapOf(Name.EMPTY to SerialPort)
else -> emptyMap()
}
public companion object : PluginFactory<SerialPortPlugin> {
override val tag: PluginTag = PluginTag("controls.ports.serial", group = PluginTag.DATAFORGE_GROUP)
override val type: KClass<out SerialPortPlugin> = SerialPortPlugin::class
override fun build(context: Context, meta: Meta): SerialPortPlugin = SerialPortPlugin()
}
}

View File

@ -12,7 +12,7 @@ val ktorVersion: String by rootProject.extra
dependencies { dependencies {
implementation(project(":controls-core")) implementation(project(":controls-core"))
implementation(project(":controls-tcp")) implementation(project(":controls-ktor-tcp"))
implementation(projects.magix.magixServer) implementation(projects.magix.magixServer)
implementation("io.ktor:ktor-server-cio:$ktorVersion") implementation("io.ktor:ktor-server-cio:$ktorVersion")
implementation("io.ktor:ktor-server-websockets:$ktorVersion") implementation("io.ktor:ktor-server-websockets:$ktorVersion")

View File

@ -0,0 +1,21 @@
plugins {
id("space.kscience.gradle.jvm")
application
}
//TODO to be moved to a separate project
//
//application{
// mainClass.set("ru.mipt.npm.devices.pimotionmaster.PiMotionMasterAppKt")
//}
kotlin{
explicitApi = null
}
val ktorVersion: String by rootProject.extra
val dataforgeVersion: String by extra
dependencies {
implementation(projects.controlsKtorTcp)
}

View File

@ -0,0 +1,102 @@
package center.sciprog.devices.mks
import kotlinx.coroutines.withTimeoutOrNull
import space.kscience.controls.ports.Ports
import space.kscience.controls.ports.SynchronousPort
import space.kscience.controls.ports.respondStringWithDelimiter
import space.kscience.controls.ports.synchronous
import space.kscience.controls.spec.*
import space.kscience.dataforge.context.Context
import space.kscience.dataforge.context.Factory
import space.kscience.dataforge.context.request
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.get
import space.kscience.dataforge.meta.int
import space.kscience.dataforge.meta.transformations.MetaConverter
class MksPdr900Device(context: Context, meta: Meta) : DeviceBySpec<MksPdr900Device>(MksPdr900Device, context, meta) {
private val portDelegate = lazy {
val ports = context.request(Ports)
ports.buildPort(meta["port"] ?: error("Port is not defined in device configuration")).synchronous()
}
private val port: SynchronousPort by portDelegate
private val address by meta.int(253)
private val channel by meta.int(5)
private val responsePattern: Regex by lazy {
("@${address}ACK(.*);FF").toRegex()
}
private suspend fun talk(requestContent: String): String? = withTimeoutOrNull(5000) {
val answer = port.respondStringWithDelimiter(String.format("@%s%s;FF", address, requestContent), ";FF")
responsePattern.matchEntire(answer)?.groups?.get(1)?.value
?: error("Message $answer does not match $responsePattern")
}
public suspend fun readPowerOn(): Boolean = when (val answer = talk("FP?")) {
"ON" -> true
"OFF" -> false
else -> error("Unknown answer for 'FP?': $answer")
}
public suspend fun writePowerOn(powerOnValue: Boolean) {
error.invalidate()
if (powerOnValue) {
val ans = talk("FP!ON")
if (ans == "ON") {
updateLogical(powerOn, true)
} else {
updateLogical(error, "Failed to set power state")
}
} else {
val ans = talk("FP!OFF")
if (ans == "OFF") {
updateLogical(powerOn, false)
} else {
updateLogical(error, "Failed to set power state")
}
}
}
public suspend fun readChannelData(): Double? {
val answer: String? = talk("PR$channel?")
error.invalidate()
return if (answer.isNullOrEmpty()) {
// updateState(PortSensor.CONNECTED_STATE, false)
updateLogical(error, "No connection")
null
} else {
val res = answer.toDouble()
if (res <= 0) {
updateLogical(powerOn, false)
updateLogical(error, "No power")
null
} else {
res
}
}
}
companion object : DeviceSpec<MksPdr900Device>(), Factory<MksPdr900Device> {
override fun build(context: Context, meta: Meta): MksPdr900Device = MksPdr900Device(context, meta)
val powerOn by booleanProperty(read = MksPdr900Device::readPowerOn, write = MksPdr900Device::writePowerOn)
val value by doubleProperty(read = MksPdr900Device::readChannelData)
val error by logicalProperty(MetaConverter.string)
override fun MksPdr900Device.onClose() {
if (portDelegate.isInitialized()) {
port.close()
}
}
}
}

View File

@ -0,0 +1,10 @@
package center.sciprog.devices.mks
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.string
import space.kscience.dataforge.meta.transformations.MetaConverter
object NullableStringMetaConverter : MetaConverter<String?> {
override fun metaToObject(meta: Meta): String? = meta.string
override fun objectToMeta(obj: String?): Meta = Meta {}
}

View File

@ -23,7 +23,7 @@ val ktorVersion: String by rootProject.extra
val dataforgeVersion: String by extra val dataforgeVersion: String by extra
dependencies { dependencies {
implementation(project(":controls-tcp")) implementation(project(":controls-ktor-tcp"))
implementation(project(":controls-magix-client")) implementation(project(":controls-magix-client"))
implementation("no.tornado:tornadofx:1.7.20") implementation("no.tornado:tornadofx:1.7.20")
} }

View File

@ -44,8 +44,8 @@ fun VBox.piMotionMasterAxis(
label(axisName) label(axisName)
coroutineScope.launch { coroutineScope.launch {
with(axis) { with(axis) {
val min = minPosition.read() val min: Double = minPosition.read()
val max = maxPosition.read() val max: Double = maxPosition.read()
val positionProperty = fxProperty(position) val positionProperty = fxProperty(position)
val startPosition = position.read() val startPosition = position.read()
runLater { runLater {

View File

@ -87,7 +87,7 @@ class PiMotionMasterDevice(
suspend fun getErrorCode(): Int = mutex.withLock { suspend fun getErrorCode(): Int = mutex.withLock {
withTimeout(timeoutValue) { withTimeout(timeoutValue) {
sendCommandInternal("ERR?") sendCommandInternal("ERR?")
val errorString = port?.receiving()?.withDelimiter("\n")?.first() ?: error("Not connected to device") val errorString = port?.receiving()?.withStringDelimiter("\n")?.first() ?: error("Not connected to device")
errorString.trim().toInt() errorString.trim().toInt()
} }
} }
@ -100,7 +100,7 @@ class PiMotionMasterDevice(
try { try {
withTimeout(timeoutValue) { withTimeout(timeoutValue) {
sendCommandInternal(command, *arguments) sendCommandInternal(command, *arguments)
val phrases = port?.receiving()?.withDelimiter("\n") ?: error("Not connected to device") val phrases = port?.receiving()?.withStringDelimiter("\n") ?: error("Not connected to device")
phrases.transformWhile { line -> phrases.transformWhile { line ->
emit(line) emit(line)
line.endsWith(" \n") line.endsWith(" \n")

View File

@ -42,7 +42,7 @@ dependencyResolutionManagement {
include( include(
":controls-core", ":controls-core",
":controls-tcp", ":controls-ktor-tcp",
":controls-serial", ":controls-serial",
":controls-server", ":controls-server",
":controls-opcua", ":controls-opcua",
@ -64,5 +64,6 @@ include(
":demo:magix-demo", ":demo:magix-demo",
":demo:car", ":demo:car",
":demo:motors", ":demo:motors",
":demo:echo" ":demo:echo",
":demo:mks-pdr900"
) )