diff --git a/build.gradle.kts b/build.gradle.kts index fd641b2..ec6fd86 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,4 +1,4 @@ -val dataforgeVersion by extra("0.1.8") +val dataforgeVersion by extra("0.1.9-dev-2") allprojects { repositories { diff --git a/dataforge-device-client/build.gradle.kts b/dataforge-device-client/build.gradle.kts index 65e7b97..6367217 100644 --- a/dataforge-device-client/build.gradle.kts +++ b/dataforge-device-client/build.gradle.kts @@ -1,19 +1,18 @@ plugins { - id("scientifik.mpp") - id("scientifik.publish") + id("kscience.mpp") + id("kscience.publish") } -val ktorVersion: String by extra("1.3.2") - +val ktorVersion: String by extra("1.4.0") kotlin { - js { - browser { - dceTask { - keep("ktor-ktor-io.\$\$importsForInline\$\$.ktor-ktor-io.io.ktor.utils.io") - } - } - } +// js { +// browser { +// dceTask { +// keep("ktor-ktor-io.\$\$importsForInline\$\$.ktor-ktor-io.io.ktor.utils.io") +// } +// } +// } sourceSets { commonMain { diff --git a/dataforge-device-client/src/commonMain/kotlin/hep/dataforge/control/client/MagixClient.kt b/dataforge-device-client/src/commonMain/kotlin/hep/dataforge/control/client/MagixClient.kt index f799277..1db6841 100644 --- a/dataforge-device-client/src/commonMain/kotlin/hep/dataforge/control/client/MagixClient.kt +++ b/dataforge-device-client/src/commonMain/kotlin/hep/dataforge/control/client/MagixClient.kt @@ -16,8 +16,7 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.collect import kotlinx.coroutines.launch -import kotlinx.serialization.json.JsonObject -import kotlinx.serialization.json.json +import kotlinx.serialization.json.* import kotlin.coroutines.CoroutineContext /* @@ -61,14 +60,14 @@ class MagixClient( } } - private fun wrapMessage(message: DeviceMessage, requestId: String? = null): JsonObject = json { - "id" to generateId(message, requestId) + private fun wrapMessage(message: DeviceMessage, requestId: String? = null): JsonObject = buildJsonObject { + put("id", generateId(message, requestId)) if (requestId != null) { - "parentId" to requestId + put("parentId", requestId) } - "target" to "magix" - "origin" to "df" - "payload" to message.config.toJson() + put("target", "magix") + put("origin", "df") + put("payload", message.config.toJson()) } @@ -81,7 +80,7 @@ class MagixClient( private val respondJob = launch { inbox.collect { json -> - val requestId = json["id"]?.primitive?.content + val requestId = json["id"]?.jsonPrimitive?.content val payload = json["payload"]?.jsonObject //TODO analyze action diff --git a/dataforge-device-core/build.gradle.kts b/dataforge-device-core/build.gradle.kts index c9465bc..8c2e9de 100644 --- a/dataforge-device-core/build.gradle.kts +++ b/dataforge-device-core/build.gradle.kts @@ -1,15 +1,14 @@ -import scientifik.useCoroutines -import scientifik.useSerialization - plugins { - id("scientifik.mpp") - id("scientifik.publish") + id("kscience.mpp") + id("kscience.publish") } val dataforgeVersion: String by rootProject.extra -useCoroutines() -useSerialization() +kscience { + useCoroutines() + useSerialization() +} kotlin { sourceSets { diff --git a/dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/api/Device.kt b/dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/api/Device.kt index abb0e13..ed3420e 100644 --- a/dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/api/Device.kt +++ b/dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/api/Device.kt @@ -3,6 +3,7 @@ package hep.dataforge.control.api import hep.dataforge.control.api.Device.Companion.DEVICE_TARGET import hep.dataforge.io.Envelope import hep.dataforge.io.EnvelopeBuilder +import hep.dataforge.io.Responder import hep.dataforge.meta.Meta import hep.dataforge.meta.MetaItem import hep.dataforge.provider.Type @@ -10,82 +11,77 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.cancel import kotlinx.io.Closeable -interface Consumer { - fun consume(message: Envelope): Unit -} - /** * General interface describing a managed Device */ @Type(DEVICE_TARGET) -interface Device : Closeable { +public interface Device : Responder, Closeable { /** * List of supported property descriptors */ - val propertyDescriptors: Collection + public val propertyDescriptors: Collection /** * List of supported action descriptors. Action is a request to the device that * may or may not change the properties */ - val actionDescriptors: Collection + public val actionDescriptors: Collection /** * The scope encompassing all operations on a device. When canceled, cancels all running processes */ - val scope: CoroutineScope + public val scope: CoroutineScope /** * Register a new property change listener for this device. * [owner] is provided optionally in order for listener to be * easily removable */ - fun registerListener(listener: DeviceListener, owner: Any? = listener) + public fun registerListener(listener: DeviceListener, owner: Any? = listener) /** * Remove all listeners belonging to the specified owner */ - fun removeListeners(owner: Any?) + public fun removeListeners(owner: Any?) /** * 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<*> + public suspend fun getProperty(propertyName: String): MetaItem<*> /** * Invalidate property and force recalculate */ - suspend fun invalidateProperty(propertyName: String) + public suspend fun invalidateProperty(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. */ - suspend fun setProperty(propertyName: String, value: MetaItem<*>) + public suspend fun setProperty(propertyName: String, value: MetaItem<*>) /** * Send an action request and suspend caller while request is being processed. * Could return null if request does not return a meaningful answer. */ - suspend fun execute(command: String, argument: MetaItem<*>? = null): MetaItem<*>? + public suspend fun execute(command: String, argument: MetaItem<*>? = null): MetaItem<*>? /** * * A request with binary data or for binary response (or both). This request does not cover basic functionality like * [setProperty], [getProperty] or [execute] and not defined for a generic device. * - * TODO implement Responder after DF 0.1.9 */ - suspend fun respond(request: Envelope): EnvelopeBuilder = error("No binary response defined") + override suspend fun respond(request: Envelope): EnvelopeBuilder = error("No binary response defined") override fun close() { scope.cancel("The device is closed") } - companion object { - const val DEVICE_TARGET = "device" + public companion object { + public const val DEVICE_TARGET: String = "device" } } -suspend fun Device.execute(name: String, meta: Meta?): MetaItem<*>? = execute(name, meta?.let { MetaItem.NodeItem(it) }) \ No newline at end of file +public suspend fun Device.execute(name: String, meta: Meta?): MetaItem<*>? = execute(name, meta?.let { MetaItem.NodeItem(it) }) \ No newline at end of file diff --git a/dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/api/DeviceHub.kt b/dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/api/DeviceHub.kt index 6966d01..2010fde 100644 --- a/dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/api/DeviceHub.kt +++ b/dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/api/DeviceHub.kt @@ -1,15 +1,14 @@ package hep.dataforge.control.api import hep.dataforge.meta.MetaItem -import hep.dataforge.names.Name -import hep.dataforge.names.toName +import hep.dataforge.names.* import hep.dataforge.provider.Provider /** * A hub that could locate multiple devices and redirect actions to them */ -interface DeviceHub : Provider { - val devices: Map//TODO use token instead of Names +public interface DeviceHub : Provider { + public val devices: Map override val defaultTarget: String get() = Device.DEVICE_TARGET @@ -17,31 +16,51 @@ interface DeviceHub : Provider { override fun provideTop(target: String): Map { if (target == Device.DEVICE_TARGET) { - return devices + return buildMap { + fun putAll(prefix: Name, hub: DeviceHub) { + hub.devices.forEach { + put(prefix + it.key, it.value) + } + } + + devices.forEach { + val name = it.key.asName() + put(name, it.value) + (it.value as? DeviceHub)?.let { hub -> + putAll(name, hub) + } + } + } } else { throw IllegalArgumentException("Target $target is not supported for $this") } } - companion object { + public companion object { } } -operator fun DeviceHub.get(deviceName: Name) = - devices[deviceName] ?: error("Device with name $deviceName not found in $this") +public operator fun DeviceHub.get(nameToken: NameToken): Device = + devices[nameToken] ?: error("Device with name $nameToken not found in $this") -operator fun DeviceHub.get(deviceName: String) = get(deviceName.toName()) - -suspend fun DeviceHub.getProperty(deviceName: Name, propertyName: String): MetaItem<*> = - this[deviceName].getProperty(propertyName) - -suspend fun DeviceHub.setProperty(deviceName: Name, propertyName: String, value: MetaItem<*>) { - this[deviceName].setProperty(propertyName, value) +public operator fun DeviceHub.get(name: Name): Device? = when { + name.isEmpty() -> this as? Device + name.length == 1 -> get(name.firstOrNull()!!) + else -> (get(name.firstOrNull()!!) as? DeviceHub)?.get(name.cutFirst()) } -suspend fun DeviceHub.execute(deviceName: Name, command: String, argument: MetaItem<*>?): MetaItem<*>? = - this[deviceName].execute(command, argument) +public operator fun DeviceHub.get(deviceName: String): Device? = get(deviceName.toName()) + +public suspend fun DeviceHub.getProperty(deviceName: Name, propertyName: String): MetaItem<*>? = + this[deviceName]?.getProperty(propertyName) + +public suspend fun DeviceHub.setProperty(deviceName: Name, propertyName: String, value: MetaItem<*>) { + this[deviceName]?.setProperty(propertyName, value) +} + +public suspend fun DeviceHub.execute(deviceName: Name, command: String, argument: MetaItem<*>?): MetaItem<*>? = + this[deviceName]?.execute(command, argument) //suspend fun DeviceHub.respond(request: Envelope): EnvelopeBuilder { diff --git a/dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/api/descriptors.kt b/dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/api/descriptors.kt index f64d61c..a138ea5 100644 --- a/dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/api/descriptors.kt +++ b/dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/api/descriptors.kt @@ -8,6 +8,7 @@ import hep.dataforge.meta.string */ class PropertyDescriptor(name: String) : Scheme() { val name by string(name) + var info by string() } /** @@ -15,6 +16,7 @@ class PropertyDescriptor(name: String) : Scheme() { */ class ActionDescriptor(name: String) : Scheme() { val name by string(name) + var info by string() //var descriptor by spec(ItemDescriptor) } diff --git a/dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/base/IsolatedDeviceProperty.kt b/dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/base/IsolatedDeviceProperty.kt index efab827..9632810 100644 --- a/dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/base/IsolatedDeviceProperty.kt +++ b/dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/base/IsolatedDeviceProperty.kt @@ -292,4 +292,4 @@ fun D.writingDouble( innerGetter, innerSetter ) -} +} \ No newline at end of file diff --git a/dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/controllers/DeviceController.kt b/dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/controllers/DeviceController.kt index 8baf83c..61ce52a 100644 --- a/dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/controllers/DeviceController.kt +++ b/dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/controllers/DeviceController.kt @@ -1,6 +1,9 @@ package hep.dataforge.control.controllers -import hep.dataforge.control.api.* +import hep.dataforge.control.api.Device +import hep.dataforge.control.api.DeviceHub +import hep.dataforge.control.api.DeviceListener +import hep.dataforge.control.api.get import hep.dataforge.control.controllers.DeviceMessage.Companion.PROPERTY_CHANGED_ACTION import hep.dataforge.io.Envelope import hep.dataforge.io.Responder @@ -80,7 +83,7 @@ class DeviceController( "source" put deviceTarget } } - return response.build() + return response.seal() } } catch (ex: Exception) { DeviceMessage.fail { diff --git a/dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/controllers/DeviceMessage.kt b/dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/controllers/DeviceMessage.kt index 73eac8a..4877daf 100644 --- a/dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/controllers/DeviceMessage.kt +++ b/dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/controllers/DeviceMessage.kt @@ -4,10 +4,10 @@ import hep.dataforge.control.controllers.DeviceController.Companion.GET_PROPERTY import hep.dataforge.io.SimpleEnvelope import hep.dataforge.meta.* import hep.dataforge.names.asName -import kotlinx.serialization.Decoder -import kotlinx.serialization.Encoder import kotlinx.serialization.KSerializer -import kotlinx.serialization.SerialDescriptor +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder class DeviceMessage : Scheme() { var source by string(key = SOURCE_KEY) diff --git a/dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/controllers/delegates.kt b/dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/controllers/delegates.kt index b315926..122c917 100644 --- a/dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/controllers/delegates.kt +++ b/dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/controllers/delegates.kt @@ -38,3 +38,9 @@ fun DeviceProperty.convert(metaConverter: MetaConverter): ReadWrite fun ReadOnlyDeviceProperty.double() = convert(MetaConverter.double) fun DeviceProperty.double() = convert(MetaConverter.double) + +fun ReadOnlyDeviceProperty.int() = convert(MetaConverter.int) +fun DeviceProperty.int() = convert(MetaConverter.int) + +fun ReadOnlyDeviceProperty.string() = convert(MetaConverter.string) +fun DeviceProperty.string() = convert(MetaConverter.string) \ No newline at end of file diff --git a/dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/ports/Port.kt b/dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/ports/Port.kt index 2c5f190..635961e 100644 --- a/dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/ports/Port.kt +++ b/dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/ports/Port.kt @@ -1,18 +1,26 @@ package hep.dataforge.control.ports -import kotlinx.coroutines.CancellationException -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Job +import kotlinx.coroutines.* import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.receiveAsFlow -import kotlinx.coroutines.launch import kotlinx.io.Closeable import mu.KLogger +import mu.KotlinLogging +import kotlin.coroutines.CoroutineContext -abstract class Port(val scope: CoroutineScope) : Closeable { +interface Port: Closeable { + suspend fun send(data: ByteArray) + suspend fun receiving(): Flow + fun isOpen(): Boolean +} - abstract val logger: KLogger + +abstract class AbstractPort(parentContext: CoroutineContext) : Port { + + protected val scope = CoroutineScope(SupervisorJob(parentContext[Job])) + + protected val logger: KLogger by lazy { KotlinLogging.logger(toString()) } private val outgoing = Channel(100) private val incoming = Channel(Channel.CONFLATED) @@ -53,7 +61,7 @@ abstract class Port(val scope: CoroutineScope) : Closeable { /** * Send a data packet via the port */ - suspend fun send(data: ByteArray) { + override suspend fun send(data: ByteArray) { outgoing.send(data) } @@ -62,7 +70,7 @@ abstract class Port(val scope: CoroutineScope) : Closeable { * In order to form phrases some condition should used on top of it. * For example [delimitedIncoming] generates phrases with fixed delimiter. */ - fun incoming(): Flow { + override suspend fun receiving(): Flow { return incoming.receiveAsFlow() } @@ -70,7 +78,10 @@ abstract class Port(val scope: CoroutineScope) : Closeable { outgoing.close() incoming.close() sendJob.cancel() + scope.cancel() } + + override fun isOpen(): Boolean = scope.isActive } /** diff --git a/dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/ports/PortProxy.kt b/dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/ports/PortProxy.kt new file mode 100644 index 0000000..2849d74 --- /dev/null +++ b/dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/ports/PortProxy.kt @@ -0,0 +1,36 @@ +package hep.dataforge.control.ports + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock + +class PortProxy(val factory: suspend () -> Port) : Port { + + private var actualPort: Port? = null + private val mutex = Mutex() + + suspend fun port(): Port{ + return mutex.withLock { + if(actualPort?.isOpen() == true){ + actualPort!! + } else { + factory().also{ + actualPort = it + } + } + } + } + + override suspend fun send(data: ByteArray) { + port().send(data) + } + + override suspend fun receiving(): Flow = port().receiving() + + // open by default + override fun isOpen(): Boolean = true + + override fun close() { + actualPort?.close() + } +} \ No newline at end of file diff --git a/dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/ports/SynchronousPortHandler.kt b/dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/ports/SynchronousPortHandler.kt index 7d81bef..1cbcb78 100644 --- a/dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/ports/SynchronousPortHandler.kt +++ b/dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/ports/SynchronousPortHandler.kt @@ -19,7 +19,7 @@ class SynchronousPortHandler(val port: Port) { suspend fun respond(data: ByteArray, transform: suspend Flow.() -> R): R { return mutex.withLock { port.send(data) - transform(port.incoming()) + transform(port.receiving()) } } } diff --git a/dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/ports/phrases.kt b/dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/ports/phrases.kt index a268326..b0d14fb 100644 --- a/dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/ports/phrases.kt +++ b/dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/ports/phrases.kt @@ -3,9 +3,13 @@ package hep.dataforge.control.ports import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.map import kotlinx.io.ByteArrayOutput -fun Flow.withDelimiter(delimiter: ByteArray, expectedMessageSize: Int = 32): Flow = flow { +/** + * Transform byte fragments into complete phrases using given delimiter + */ +public fun Flow.withDelimiter(delimiter: ByteArray, expectedMessageSize: Int = 32): Flow = flow { require(delimiter.isNotEmpty()) { "Delimiter must not be empty" } var output = ByteArrayOutput(expectedMessageSize) @@ -31,8 +35,15 @@ fun Flow.withDelimiter(delimiter: ByteArray, expectedMessageSize: Int } } +/** + * Transform byte fragments into utf-8 phrases using utf-8 delimiter + */ +public fun Flow.withDelimiter(delimiter: String, expectedMessageSize: Int = 32): Flow { + return withDelimiter(delimiter.encodeToByteArray()).map { it.decodeToString() } +} + /** * A flow of delimited phrases */ -fun Port.delimitedIncoming(delimiter: ByteArray, expectedMessageSize: Int = 32) = - incoming().withDelimiter(delimiter, expectedMessageSize) +public suspend fun Port.delimitedIncoming(delimiter: ByteArray, expectedMessageSize: Int = 32): Flow = + receiving().withDelimiter(delimiter, expectedMessageSize) diff --git a/dataforge-device-core/src/jvmMain/kotlin/hep/dataforge/control/ports/KtorTcpPort.kt b/dataforge-device-core/src/jvmMain/kotlin/hep/dataforge/control/ports/KtorTcpPort.kt index 5c610d5..0ec61a1 100644 --- a/dataforge-device-core/src/jvmMain/kotlin/hep/dataforge/control/ports/KtorTcpPort.kt +++ b/dataforge-device-core/src/jvmMain/kotlin/hep/dataforge/control/ports/KtorTcpPort.kt @@ -6,19 +6,21 @@ import io.ktor.network.sockets.openReadChannel import io.ktor.network.sockets.openWriteChannel import io.ktor.utils.io.consumeEachBufferRange import io.ktor.utils.io.writeAvailable -import kotlinx.coroutines.* -import mu.KLogger -import mu.KotlinLogging +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch import java.net.InetSocketAddress +import kotlin.coroutines.CoroutineContext import kotlin.coroutines.coroutineContext class KtorTcpPort internal constructor( - scope: CoroutineScope, + parentContext: CoroutineContext, val host: String, val port: Int -) : Port(scope), AutoCloseable { +) : AbstractPort(parentContext), AutoCloseable { - override val logger: KLogger = KotlinLogging.logger("port[tcp:$host:$port]") + override fun toString() = "port[tcp:$host:$port]" private val futureSocket = scope.async { aSocket(ActorSelectorManager(Dispatchers.IO)).tcp().connect(InetSocketAddress(host, port)) @@ -48,10 +50,9 @@ class KtorTcpPort internal constructor( super.close() } - companion object{ - suspend fun open(host: String, port: Int): KtorTcpPort{ - val scope = CoroutineScope(SupervisorJob(coroutineContext[Job])) - return KtorTcpPort(scope, host, port) + companion object { + suspend fun open(host: String, port: Int): KtorTcpPort { + return KtorTcpPort(coroutineContext, host, port) } } } \ No newline at end of file diff --git a/dataforge-device-core/src/jvmMain/kotlin/hep/dataforge/control/ports/TcpPort.kt b/dataforge-device-core/src/jvmMain/kotlin/hep/dataforge/control/ports/TcpPort.kt index 942aea1..2049c3c 100644 --- a/dataforge-device-core/src/jvmMain/kotlin/hep/dataforge/control/ports/TcpPort.kt +++ b/dataforge-device-core/src/jvmMain/kotlin/hep/dataforge/control/ports/TcpPort.kt @@ -1,11 +1,10 @@ package hep.dataforge.control.ports import kotlinx.coroutines.* -import mu.KLogger -import mu.KotlinLogging import java.net.InetSocketAddress import java.nio.ByteBuffer import java.nio.channels.SocketChannel +import kotlin.coroutines.CoroutineContext import kotlin.coroutines.coroutineContext internal fun ByteBuffer.readArray(limit: Int = limit()): ByteArray { @@ -17,12 +16,12 @@ internal fun ByteBuffer.readArray(limit: Int = limit()): ByteArray { } class TcpPort private constructor( - scope: CoroutineScope, + parentContext: CoroutineContext, val host: String, val port: Int -) : Port(scope), AutoCloseable { +) : AbstractPort(parentContext), AutoCloseable { - override val logger: KLogger = KotlinLogging.logger("port[tcp:$host:$port]") + override fun toString(): String = "port[tcp:$host:$port]" private val futureChannel: Deferred = this.scope.async(Dispatchers.IO) { SocketChannel.open(InetSocketAddress(host, port)).apply { @@ -64,8 +63,7 @@ class TcpPort private constructor( companion object{ suspend fun open(host: String, port: Int): TcpPort{ - val scope = CoroutineScope(SupervisorJob(coroutineContext[Job])) - return TcpPort(scope, host, port) + return TcpPort(coroutineContext, host, port) } } } \ No newline at end of file diff --git a/dataforge-device-core/src/jvmTest/kotlin/hep/dataforge/control/ports/TcpPortTest.kt b/dataforge-device-core/src/jvmTest/kotlin/hep/dataforge/control/ports/TcpPortTest.kt index 36a60e0..0f25b53 100644 --- a/dataforge-device-core/src/jvmTest/kotlin/hep/dataforge/control/ports/TcpPortTest.kt +++ b/dataforge-device-core/src/jvmTest/kotlin/hep/dataforge/control/ports/TcpPortTest.kt @@ -58,7 +58,7 @@ class TcpPortTest { val port = TcpPort.open("localhost", 22188) val logJob = launch { - port.incoming().collect { + port.receiving().collect { println("Flow: ${it.decodeToString()}") } } @@ -83,7 +83,7 @@ class TcpPortTest { val port = KtorTcpPort.open("localhost", 22188) val logJob = launch { - port.incoming().collect { + port.receiving().collect { println("Flow: ${it.decodeToString()}") } } diff --git a/dataforge-device-serial/build.gradle.kts b/dataforge-device-serial/build.gradle.kts index b956e43..6d033ae 100644 --- a/dataforge-device-serial/build.gradle.kts +++ b/dataforge-device-serial/build.gradle.kts @@ -1,6 +1,6 @@ plugins { - id("scientifik.jvm") - id("scientifik.publish") + id("kscience.jvm") + id("kscience.publish") } dependencies{ diff --git a/dataforge-device-serial/src/main/kotlin/hep/dataforge/control/serial/SerialPort.kt b/dataforge-device-serial/src/main/kotlin/hep/dataforge/control/serial/SerialPort.kt index 0e5ab34..989aa1a 100644 --- a/dataforge-device-serial/src/main/kotlin/hep/dataforge/control/serial/SerialPort.kt +++ b/dataforge-device-serial/src/main/kotlin/hep/dataforge/control/serial/SerialPort.kt @@ -1,21 +1,19 @@ package hep.dataforge.control.serial -import hep.dataforge.control.ports.Port +import hep.dataforge.control.ports.AbstractPort import jssc.SerialPort.* import jssc.SerialPortEventListener -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Job -import kotlinx.coroutines.SupervisorJob -import mu.KLogger -import mu.KotlinLogging +import kotlin.coroutines.CoroutineContext import kotlin.coroutines.coroutineContext import jssc.SerialPort as JSSCPort /** * COM/USB port */ -class SerialPort private constructor(scope: CoroutineScope, val jssc: JSSCPort) : Port(scope) { - override val logger: KLogger = KotlinLogging.logger("port[${jssc.portName}]") +public class SerialPort private constructor(parentContext: CoroutineContext, private val jssc: JSSCPort) : + AbstractPort(parentContext) { + + override fun toString(): String = "port[${jssc.portName}]" private val serialPortListener = SerialPortEventListener { event -> if (event.isRXCHAR) { @@ -32,7 +30,7 @@ class SerialPort private constructor(scope: CoroutineScope, val jssc: JSSCPort) /** * Clear current input and output buffers */ - fun clearPort() { + internal fun clearPort() { jssc.purgePort(PURGE_RXCLEAR or PURGE_TXCLEAR) } @@ -50,24 +48,23 @@ class SerialPort private constructor(scope: CoroutineScope, val jssc: JSSCPort) super.close() } - companion object { + public companion object { /** * Construct ComPort with given parameters */ - suspend fun open( + public suspend fun open( portName: String, baudRate: Int = BAUDRATE_9600, dataBits: Int = DATABITS_8, stopBits: Int = STOPBITS_1, - parity: Int = PARITY_NONE + parity: Int = PARITY_NONE, ): SerialPort { val jssc = JSSCPort(portName).apply { openPort() setParams(baudRate, dataBits, stopBits, parity) } - val scope = CoroutineScope(SupervisorJob(coroutineContext[Job])) - return SerialPort(scope, jssc) + return SerialPort(coroutineContext, jssc) } } } \ No newline at end of file diff --git a/dataforge-device-server/build.gradle.kts b/dataforge-device-server/build.gradle.kts index a04075f..7e0c1ce 100644 --- a/dataforge-device-server/build.gradle.kts +++ b/dataforge-device-server/build.gradle.kts @@ -1,11 +1,11 @@ -import scientifik.useSerialization - plugins { - id("scientifik.jvm") - id("scientifik.publish") + id("kscience.jvm") + id("kscience.publish") } -useSerialization() +kscience { + useSerialization() +} val dataforgeVersion: String by rootProject.extra val ktorVersion: String by extra("1.3.2") diff --git a/dataforge-device-server/src/main/kotlin/hep/dataforge/control/server/conversions.kt b/dataforge-device-server/src/main/kotlin/hep/dataforge/control/server/conversions.kt index 7bc25eb..4b3f369 100644 --- a/dataforge-device-server/src/main/kotlin/hep/dataforge/control/server/conversions.kt +++ b/dataforge-device-server/src/main/kotlin/hep/dataforge/control/server/conversions.kt @@ -8,36 +8,36 @@ import io.ktor.http.ContentType import io.ktor.http.cio.websocket.Frame import io.ktor.response.respondText import kotlinx.io.asBinary -import kotlinx.serialization.UnstableDefault import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonObjectBuilder -import kotlinx.serialization.json.json +import kotlinx.serialization.json.buildJsonObject -fun Frame.toEnvelope(): Envelope { + +internal fun Frame.toEnvelope(): Envelope { return data.asBinary().readWith(TaggedEnvelopeFormat) } -fun Envelope.toFrame(): Frame { +internal fun Envelope.toFrame(): Frame { val data = buildByteArray { - writeWith(TaggedEnvelopeFormat,this@toFrame) + writeWith(TaggedEnvelopeFormat, this@toFrame) } return Frame.Binary(false, data) } -suspend fun ApplicationCall.respondJson(builder: JsonObjectBuilder.() -> Unit) { - val json = json(builder) +internal suspend fun ApplicationCall.respondJson(builder: JsonObjectBuilder.() -> Unit) { + val json = buildJsonObject(builder) respondText(json.toString(), contentType = ContentType.Application.Json) } -@OptIn(UnstableDefault::class) -suspend fun ApplicationCall.respondMessage(message: DeviceMessage) { - respondText(Json.stringify(MetaSerializer,message.toMeta()), contentType = ContentType.Application.Json) + +public suspend fun ApplicationCall.respondMessage(message: DeviceMessage) { + respondText(Json.encodeToString(MetaSerializer, message.toMeta()), contentType = ContentType.Application.Json) } -suspend fun ApplicationCall.respondMessage(builder: DeviceMessage.() -> Unit) { +public suspend fun ApplicationCall.respondMessage(builder: DeviceMessage.() -> Unit) { respondMessage(DeviceMessage(builder)) } -suspend fun ApplicationCall.respondFail(builder: DeviceMessage.() -> Unit) { +public suspend fun ApplicationCall.respondFail(builder: DeviceMessage.() -> Unit) { respondMessage(DeviceMessage.fail(null, builder)) } \ No newline at end of file diff --git a/dataforge-device-server/src/main/kotlin/hep/dataforge/control/server/deviceWebServer.kt b/dataforge-device-server/src/main/kotlin/hep/dataforge/control/server/deviceWebServer.kt index 456e7d5..bfe2bd6 100644 --- a/dataforge-device-server/src/main/kotlin/hep/dataforge/control/server/deviceWebServer.kt +++ b/dataforge-device-server/src/main/kotlin/hep/dataforge/control/server/deviceWebServer.kt @@ -1,4 +1,4 @@ -@file:OptIn(ExperimentalCoroutinesApi::class, KtorExperimentalAPI::class, FlowPreview::class, UnstableDefault::class) +@file:OptIn(ExperimentalCoroutinesApi::class, KtorExperimentalAPI::class, FlowPreview::class) package hep.dataforge.control.server @@ -35,15 +35,16 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.collect import kotlinx.html.* -import kotlinx.serialization.UnstableDefault import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonObject -import kotlinx.serialization.json.jsonArray +import kotlinx.serialization.json.buildJsonArray +import kotlinx.serialization.json.put /** * Create and start a web server for several devices */ -fun CoroutineScope.startDeviceServer( +@OptIn(KtorExperimentalAPI::class) +public fun CoroutineScope.startDeviceServer( manager: DeviceManager, port: Int = 8111, host: String = "localhost" @@ -54,9 +55,6 @@ fun CoroutineScope.startDeviceServer( install(CORS) { anyHost() } -// install(ContentNegotiation) { -// json() -// } install(StatusPages) { exception { cause -> call.respond(HttpStatusCode.BadRequest, cause.message ?: "") @@ -71,15 +69,15 @@ fun CoroutineScope.startDeviceServer( }.start() } -fun ApplicationEngine.whenStarted(callback: Application.() -> Unit) { +public fun ApplicationEngine.whenStarted(callback: Application.() -> Unit) { environment.monitor.subscribe(ApplicationStarted, callback) } -const val WEB_SERVER_TARGET = "@webServer" +public const val WEB_SERVER_TARGET: String = "@webServer" @OptIn(KtorExperimentalAPI::class) -fun Application.deviceModule( +public fun Application.deviceModule( manager: DeviceManager, deviceNames: Collection = manager.devices.keys.map { it.toString() }, route: String = "/" @@ -152,17 +150,17 @@ fun Application.deviceModule( get("list") { call.respondJson { manager.devices.forEach { (name, device) -> - "target" to name.toString() - "properties" to jsonArray { + put("target", name.toString()) + put("properties", buildJsonArray { device.propertyDescriptors.forEach { descriptor -> - +descriptor.config.toJson() + add(descriptor.config.toJson()) } - } - "actions" to jsonArray { + }) + put("actions", buildJsonArray { device.actionDescriptors.forEach { actionDescriptor -> - +actionDescriptor.config.toJson() + add(actionDescriptor.config.toJson()) } - } + }) } } } @@ -187,7 +185,7 @@ fun Application.deviceModule( post("message") { val body = call.receiveText() - val json = Json.parseJson(body) as? JsonObject + val json = Json.parseToJsonElement(body) as? JsonObject ?: throw IllegalArgumentException("The body is not a json object") val meta = json.toMeta() @@ -220,7 +218,7 @@ fun Application.deviceModule( val target: String by call.parameters val property: String by call.parameters val body = call.receiveText() - val json = Json.parseJson(body) + val json = Json.parseToJsonElement(body) val request = DeviceMessage { type = SET_PROPERTY_ACTION diff --git a/dataforge-device-server/src/main/kotlin/hep/dataforge/control/server/sse.kt b/dataforge-device-server/src/main/kotlin/hep/dataforge/control/server/sse.kt index 34ce2c1..95dffec 100644 --- a/dataforge-device-server/src/main/kotlin/hep/dataforge/control/server/sse.kt +++ b/dataforge-device-server/src/main/kotlin/hep/dataforge/control/server/sse.kt @@ -12,7 +12,7 @@ import kotlinx.coroutines.flow.collect /** * The data class representing a SSE Event that will be sent to the client. */ -data class SseEvent(val data: String, val event: String? = null, val id: String? = null) +public data class SseEvent(val data: String, val event: String? = null, val id: String? = null) /** * Method that responds an [ApplicationCall] by reading all the [SseEvent]s from the specified [events] [ReceiveChannel] @@ -21,7 +21,7 @@ data class SseEvent(val data: String, val event: String? = null, val id: String? * You can read more about it here: https://www.html5rocks.com/en/tutorials/eventsource/basics/ */ @Suppress("BlockingMethodInNonBlockingContext") -suspend fun ApplicationCall.respondSse(events: Flow) { +public suspend fun ApplicationCall.respondSse(events: Flow) { response.cacheControl(CacheControl.NoCache(null)) respondTextWriter(contentType = ContentType.Text.EventStream) { events.collect { event-> diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 62d4c05..e708b1c 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 622ab64..12d38de 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.6.1-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index fbd7c51..4f906e0 100755 --- a/gradlew +++ b/gradlew @@ -130,7 +130,7 @@ fi if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then APP_HOME=`cygpath --path --mixed "$APP_HOME"` CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - + JAVACMD=`cygpath --unix "$JAVACMD"` # We build the pattern for arguments to be converted via cygpath diff --git a/gradlew.bat b/gradlew.bat index 5093609..107acd3 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -40,7 +40,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto init +if "%ERRORLEVEL%" == "0" goto execute echo. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. @@ -54,7 +54,7 @@ goto fail set JAVA_HOME=%JAVA_HOME:"=% set JAVA_EXE=%JAVA_HOME%/bin/java.exe -if exist "%JAVA_EXE%" goto init +if exist "%JAVA_EXE%" goto execute echo. echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% @@ -64,21 +64,6 @@ echo location of your Java installation. goto fail -:init -@rem Get command-line arguments, handling Windows variants - -if not "%OS%" == "Windows_NT" goto win9xME_args - -:win9xME_args -@rem Slurp the command line arguments. -set CMD_LINE_ARGS= -set _SKIP=2 - -:win9xME_args_slurp -if "x%~1" == "x" goto execute - -set CMD_LINE_ARGS=%* - :execute @rem Setup the command line @@ -86,7 +71,7 @@ set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar @rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* :end @rem End local scope for the variables with windows NT shell diff --git a/motors/build.gradle.kts b/motors/build.gradle.kts index 1599ffb..97317c4 100644 --- a/motors/build.gradle.kts +++ b/motors/build.gradle.kts @@ -1,6 +1,6 @@ plugins { - id("scientifik.jvm") - id("scientifik.publish") + id("kscience.jvm") + id("kscience.publish") } //TODO to be moved to a separate project diff --git a/motors/docs/C885T0002-TN-C-885.PIMotionMaster-EN.pdf b/motors/docs/C885T0002-TN-C-885.PIMotionMaster-EN.pdf new file mode 100644 index 0000000..61bb505 Binary files /dev/null and b/motors/docs/C885T0002-TN-C-885.PIMotionMaster-EN.pdf differ diff --git a/motors/src/main/kotlin/ru/mipt/npm/devices/pimotionmaster/PiMotionMasterDevice.kt b/motors/src/main/kotlin/ru/mipt/npm/devices/pimotionmaster/PiMotionMasterDevice.kt new file mode 100644 index 0000000..6ae69da --- /dev/null +++ b/motors/src/main/kotlin/ru/mipt/npm/devices/pimotionmaster/PiMotionMasterDevice.kt @@ -0,0 +1,36 @@ +package ru.mipt.npm.devices.pimotionmaster + +import hep.dataforge.control.base.DeviceBase +import hep.dataforge.control.base.DeviceProperty +import hep.dataforge.control.base.writingVirtual +import hep.dataforge.control.ports.Port +import hep.dataforge.control.ports.PortProxy +import hep.dataforge.control.ports.withDelimiter +import hep.dataforge.meta.MetaItem +import hep.dataforge.values.Null +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.first + +class PiMotionMasterDevice(parentScope: CoroutineScope, val portFactory: suspend (MetaItem<*>?) -> Port) : DeviceBase() { + override val scope: CoroutineScope = CoroutineScope( + parentScope.coroutineContext + Job(parentScope.coroutineContext[Job]) + ) + + public val port: DeviceProperty by writingVirtual(Null) { + info = "The port for TCP connector" + } + + private val connector = PortProxy { portFactory(port.value) } + + private suspend fun readPhrase(command: String) { + connector.receiving().withDelimiter("\n").first { it.startsWith(command) } + } + +// +// val firmwareVersion by reading { +// connector.r +// } + + +} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index c0294be..02ed088 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,6 +1,6 @@ pluginManagement { - val kotlinVersion = "1.3.72" - val toolsVersion = "0.5.2" + val kotlinVersion = "1.4.0" + val toolsVersion = "0.6.0" repositories { mavenLocal() @@ -24,7 +24,7 @@ pluginManagement { resolutionStrategy { eachPlugin { when (requested.id.id) { - "scientifik.publish", "scientifik.mpp", "scientifik.jvm", "scientifik.js" -> useModule("scientifik:gradle-tools:${toolsVersion}") + "kscience.publish", "kscience.mpp", "kscience.jvm", "kscience.js" -> useModule("ru.mipt.npm:gradle-tools:${toolsVersion}") "kotlinx-atomicfu" -> useModule("org.jetbrains.kotlinx:atomicfu-gradle-plugin:${requested.version}") } }