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

View File

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

View File

@ -6,11 +6,19 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.receiveAsFlow
import space.kscience.controls.api.Socket
import space.kscience.dataforge.context.*
import space.kscience.dataforge.misc.Type
import kotlin.coroutines.CoroutineContext
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(
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.
* 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.
*/
override fun receiving(): Flow<ByteArray> {
return incoming.receiveAsFlow()
}
override fun receiving(): Flow<ByteArray> = incoming.receiveAsFlow()
override fun 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.
* 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) {
private val mutex = Mutex()
public class SynchronousPort(public val port: Port, private val mutex: Mutex) : Port by port {
/**
* 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 {
return mutex.withLock {
port.send(data)
transform(port.receiving())
}
public suspend fun <R> respond(data: ByteArray, transform: suspend Flow<ByteArray>.() -> R): R = mutex.withLock {
port.send(data)
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
*/
public suspend fun SynchronousPortHandler.respondWithDelimiter(data: ByteArray, delimiter: ByteArray): ByteArray {
return respond(data) {
withDelimiter(delimiter).first()
}
public suspend fun SynchronousPort.respondWithDelimiter(
data: ByteArray,
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
*/
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() }
}
/**
* A flow of delimited phrases
*/
public suspend fun Port.delimitedIncoming(delimiter: ByteArray): Flow<ByteArray> =
receiving().withDelimiter(delimiter)
public fun Port.delimitedIncoming(delimiter: ByteArray): Flow<ByteArray> = receiving().withDelimiter(delimiter)

View File

@ -17,10 +17,17 @@ import kotlin.coroutines.CoroutineContext
@OptIn(InternalDeviceAPI::class)
public abstract class DeviceBase<D : DeviceBase<D>>(
override val context: Context = Global,
override val meta: Meta = Meta.EMPTY
override val meta: Meta = Meta.EMPTY,
) : Device {
/**
* Collection of property specifications
*/
public abstract val properties: Map<String, DevicePropertySpec<D, *>>
/**
* Collection of action specifications
*/
public abstract val actions: Map<String, DeviceActionSpec<D, *, *>>
override val propertyDescriptors: Collection<PropertyDescriptor>
@ -33,6 +40,9 @@ public abstract class DeviceBase<D : DeviceBase<D>>(
context.coroutineContext + SupervisorJob(context.coroutineContext[Job])
}
/**
* Logical state store
*/
private val logicalState: HashMap<String, Meta?> = HashMap()
private val sharedMessageFlow: MutableSharedFlow<DeviceMessage> = MutableSharedFlow()
@ -99,14 +109,18 @@ public abstract class DeviceBase<D : DeviceBase<D>>(
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 {
val res = read(self)
public suspend fun <T> DevicePropertySpec<D, T>.readOrNull(): T? {
val res = read(self) ?: return null
updateLogical(name, converter.objectToMeta(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)
/**
@ -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)
}
@ -132,17 +153,17 @@ public abstract class DeviceBase<D : DeviceBase<D>>(
public open class DeviceBySpec<D : DeviceBySpec<D>>(
public val spec: DeviceSpec<in D>,
context: Context = Global,
meta: Meta = Meta.EMPTY
meta: Meta = Meta.EMPTY,
) : DeviceBase<D>(context, meta) {
override val properties: Map<String, DevicePropertySpec<D, *>> get() = spec.properties
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()
self.onOpen()
}
override fun close(): Unit = with(spec){
override fun close(): Unit = with(spec) {
self.onClose()
super.close()
}

View File

@ -35,7 +35,7 @@ public interface DevicePropertySpec<in D : Device, T> {
* Read physical value from the given [device]
*/
@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
@OptIn(InternalDeviceAPI::class)
public suspend fun <D : Device, T> DevicePropertySpec<D, T>.readMeta(device: D): Meta =
converter.objectToMeta(read(device))
public suspend fun <D : Device, T> DevicePropertySpec<D, T>.readMeta(device: D): Meta? =
read(device)?.let(converter::objectToMeta)
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 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
return deviceProperty
}
public fun <T : Any> registerProperty(
public fun <T> registerProperty(
converter: MetaConverter<T>,
readOnlyProperty: KProperty1<D, T>,
descriptorBuilder: PropertyDescriptor.() -> Unit = {}
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
): 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 suspend fun read(device: D): T =
withContext(device.coroutineContext) { readOnlyProperty.get(device) }
@ -50,11 +51,11 @@ public abstract class DeviceSpec<D : Device> {
return registerProperty(deviceProperty)
}
public fun <T : Any> property(
public fun <T> property(
converter: MetaConverter<T>,
readOnlyProperty: KProperty1<D, T>,
descriptorBuilder: PropertyDescriptor.() -> Unit = {}
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<Any?,DevicePropertySpec<D, T>>> =
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<Any?, DevicePropertySpec<D, T>>> =
PropertyDelegateProvider { _, property ->
val deviceProperty = object : DevicePropertySpec<D, T> {
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>,
readWriteProperty: KMutableProperty1<D, T>,
descriptorBuilder: PropertyDescriptor.() -> Unit = {}
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<Any?, WritableDevicePropertySpec<D, T>>> =
PropertyDelegateProvider { _, property ->
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>,
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
name: String? = null,
read: suspend D.() -> T
read: suspend D.() -> T?,
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DevicePropertySpec<D, T>>> =
PropertyDelegateProvider { _: DeviceSpec<D>, property ->
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 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)
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>,
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
name: String? = null,
read: suspend D.() -> T,
write: suspend D.(T) -> Unit
read: suspend D.() -> T?,
write: suspend D.(T) -> Unit,
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, WritableDevicePropertySpec<D, T>>> =
PropertyDelegateProvider { _: DeviceSpec<D>, property: KProperty<*> ->
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 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) {
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
return deviceAction
}
public fun <I : Any, O : Any> action(
public fun <I, O> action(
inputConverter: MetaConverter<I>,
outputConverter: MetaConverter<O>,
descriptorBuilder: ActionDescriptor.() -> Unit = {},
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>, property ->
val actionName = name ?: property.name
@ -185,15 +186,16 @@ public abstract class DeviceSpec<D : Device> {
public fun metaAction(
descriptorBuilder: ActionDescriptor.() -> Unit = {},
name: String? = null,
execute: suspend D.(Meta?) -> Meta?
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DeviceActionSpec<D, Meta, Meta>>> = action(
MetaConverter.Companion.meta,
MetaConverter.Companion.meta,
descriptorBuilder,
name
){
execute(it)
}
execute: suspend D.(Meta?) -> Meta?,
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DeviceActionSpec<D, Meta, Meta>>> =
action(
MetaConverter.Companion.meta,
MetaConverter.Companion.meta,
descriptorBuilder,
name
) {
execute(it)
}
/**
* An action that takes no parameters and returns no values
@ -201,14 +203,47 @@ public abstract class DeviceSpec<D : Device> {
public fun unitAction(
descriptorBuilder: ActionDescriptor.() -> Unit = {},
name: String? = null,
execute: suspend D.() -> Unit
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DeviceActionSpec<D, Meta, Meta>>> = action(
MetaConverter.Companion.meta,
MetaConverter.Companion.meta,
descriptorBuilder,
name
){
execute()
null
}
execute: suspend D.() -> Unit,
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DeviceActionSpec<D, Meta, Meta>>> =
action(
MetaConverter.Companion.meta,
MetaConverter.Companion.meta,
descriptorBuilder,
name
) {
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(
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
name: String? = null,
read: suspend D.() -> Boolean
read: suspend D.() -> Boolean?
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DevicePropertySpec<D, Boolean>>> = property(
MetaConverter.boolean,
{
@ -38,7 +38,7 @@ private inline fun numberDescriptor(
public fun <D : DeviceBase<D>> DeviceSpec<D>.numberProperty(
name: String? = null,
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
read: suspend D.() -> Number
read: suspend D.() -> Number?
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DevicePropertySpec<D, Number>>> = property(
MetaConverter.number,
numberDescriptor(descriptorBuilder),
@ -49,7 +49,7 @@ public fun <D : DeviceBase<D>> DeviceSpec<D>.numberProperty(
public fun <D : DeviceBase<D>> DeviceSpec<D>.doubleProperty(
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
name: String? = null,
read: suspend D.() -> Double
read: suspend D.() -> Double?
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DevicePropertySpec<D, Double>>> = property(
MetaConverter.double,
numberDescriptor(descriptorBuilder),
@ -60,7 +60,7 @@ public fun <D : DeviceBase<D>> DeviceSpec<D>.doubleProperty(
public fun <D : DeviceBase<D>> DeviceSpec<D>.stringProperty(
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
name: String? = null,
read: suspend D.() -> String
read: suspend D.() -> String?
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DevicePropertySpec<D, String>>> = property(
MetaConverter.string,
{
@ -76,7 +76,7 @@ public fun <D : DeviceBase<D>> DeviceSpec<D>.stringProperty(
public fun <D : DeviceBase<D>> DeviceSpec<D>.metaProperty(
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
name: String? = null,
read: suspend D.() -> Meta
read: suspend D.() -> Meta?
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DevicePropertySpec<D, Meta>>> = property(
MetaConverter.meta,
{
@ -94,7 +94,7 @@ public fun <D : DeviceBase<D>> DeviceSpec<D>.metaProperty(
public fun <D : DeviceBase<D>> DeviceSpec<D>.booleanProperty(
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
name: String? = null,
read: suspend D.() -> Boolean,
read: suspend D.() -> Boolean?,
write: suspend D.(Boolean) -> Unit
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, WritableDevicePropertySpec<D, Boolean>>> =
mutableProperty(

View File

@ -52,20 +52,20 @@ public class TcpPort private constructor(
}
if (num < 0) cancel("The input channel is exhausted")
} catch (ex: Exception) {
logger.error(ex){"Channel read error"}
logger.error(ex) { "Channel read error" }
delay(1000)
}
}
}
override suspend fun write(data: ByteArray) {
override suspend fun write(data: ByteArray): Unit = withContext(Dispatchers.IO){
futureChannel.await().write(ByteBuffer.wrap(data))
}
@OptIn(ExperimentalCoroutinesApi::class)
override fun close() {
listenerJob.cancel()
if(futureChannel.isCompleted){
if (futureChannel.isCompleted) {
futureChannel.getCompleted().close()
} else {
futureChannel.cancel()
@ -74,6 +74,9 @@ public class TcpPort private constructor(
}
public companion object : PortFactory {
override val type: String = "tcp"
public fun open(
context: Context,
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(
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
dependencies {
api(project(":controls-core"))
api(projects.controlsCore)
api("io.ktor:ktor-network:$ktorVersion")
}

View File

@ -55,7 +55,10 @@ public class KtorTcpPort internal constructor(
super.close()
}
public companion object: PortFactory {
public companion object : PortFactory {
override val type: String = "tcp"
public fun open(
context: Context,
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 {
override val type: String = "com"
/**
* 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 {
implementation(project(":controls-core"))
implementation(project(":controls-tcp"))
implementation(project(":controls-ktor-tcp"))
implementation(projects.magix.magixServer)
implementation("io.ktor:ktor-server-cio:$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
dependencies {
implementation(project(":controls-tcp"))
implementation(project(":controls-ktor-tcp"))
implementation(project(":controls-magix-client"))
implementation("no.tornado:tornadofx:1.7.20")
}

View File

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

View File

@ -87,7 +87,7 @@ class PiMotionMasterDevice(
suspend fun getErrorCode(): Int = mutex.withLock {
withTimeout(timeoutValue) {
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()
}
}
@ -100,7 +100,7 @@ class PiMotionMasterDevice(
try {
withTimeout(timeoutValue) {
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 ->
emit(line)
line.endsWith(" \n")

View File

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