Migrate to K-1.4.0
This commit is contained in:
parent
20079e62da
commit
0e6363048e
@ -1,4 +1,4 @@
|
||||
val dataforgeVersion by extra("0.1.8")
|
||||
val dataforgeVersion by extra("0.1.9-dev-2")
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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<PropertyDescriptor>
|
||||
public val propertyDescriptors: Collection<PropertyDescriptor>
|
||||
|
||||
/**
|
||||
* List of supported action descriptors. Action is a request to the device that
|
||||
* may or may not change the properties
|
||||
*/
|
||||
val actionDescriptors: Collection<ActionDescriptor>
|
||||
public val actionDescriptors: Collection<ActionDescriptor>
|
||||
|
||||
/**
|
||||
* 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) })
|
||||
public suspend fun Device.execute(name: String, meta: Meta?): MetaItem<*>? = execute(name, meta?.let { MetaItem.NodeItem(it) })
|
@ -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<Name, Device>//TODO use token instead of Names
|
||||
public interface DeviceHub : Provider {
|
||||
public val devices: Map<NameToken, Device>
|
||||
|
||||
override val defaultTarget: String get() = Device.DEVICE_TARGET
|
||||
|
||||
@ -17,31 +16,51 @@ interface DeviceHub : Provider {
|
||||
|
||||
override fun provideTop(target: String): Map<Name, Any> {
|
||||
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 {
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
|
@ -38,3 +38,9 @@ fun <T : Any> DeviceProperty.convert(metaConverter: MetaConverter<T>): 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)
|
@ -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<ByteArray>
|
||||
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<ByteArray>(100)
|
||||
private val incoming = Channel<ByteArray>(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<ByteArray> {
|
||||
override suspend fun receiving(): Flow<ByteArray> {
|
||||
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
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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<ByteArray> = port().receiving()
|
||||
|
||||
// open by default
|
||||
override fun isOpen(): Boolean = true
|
||||
|
||||
override fun close() {
|
||||
actualPort?.close()
|
||||
}
|
||||
}
|
@ -19,7 +19,7 @@ class SynchronousPortHandler(val port: Port) {
|
||||
suspend fun <R> respond(data: ByteArray, transform: suspend Flow<ByteArray>.() -> R): R {
|
||||
return mutex.withLock {
|
||||
port.send(data)
|
||||
transform(port.incoming())
|
||||
transform(port.receiving())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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<ByteArray>.withDelimiter(delimiter: ByteArray, expectedMessageSize: Int = 32): Flow<ByteArray> = flow {
|
||||
/**
|
||||
* Transform byte fragments into complete phrases using given delimiter
|
||||
*/
|
||||
public fun Flow<ByteArray>.withDelimiter(delimiter: ByteArray, expectedMessageSize: Int = 32): Flow<ByteArray> = flow {
|
||||
require(delimiter.isNotEmpty()) { "Delimiter must not be empty" }
|
||||
|
||||
var output = ByteArrayOutput(expectedMessageSize)
|
||||
@ -31,8 +35,15 @@ fun Flow<ByteArray>.withDelimiter(delimiter: ByteArray, expectedMessageSize: Int
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform byte fragments into utf-8 phrases using utf-8 delimiter
|
||||
*/
|
||||
public fun Flow<ByteArray>.withDelimiter(delimiter: String, expectedMessageSize: Int = 32): Flow<String> {
|
||||
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<ByteArray> =
|
||||
receiving().withDelimiter(delimiter, expectedMessageSize)
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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<SocketChannel> = 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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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()}")
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
plugins {
|
||||
id("scientifik.jvm")
|
||||
id("scientifik.publish")
|
||||
id("kscience.jvm")
|
||||
id("kscience.publish")
|
||||
}
|
||||
|
||||
dependencies{
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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")
|
||||
|
@ -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))
|
||||
}
|
@ -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<IllegalArgumentException> { 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<String> = 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
|
||||
|
@ -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<SseEvent>) {
|
||||
public suspend fun ApplicationCall.respondSse(events: Flow<SseEvent>) {
|
||||
response.cacheControl(CacheControl.NoCache(null))
|
||||
respondTextWriter(contentType = ContentType.Text.EventStream) {
|
||||
events.collect { event->
|
||||
|
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Binary file not shown.
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@ -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
|
||||
|
21
gradlew.bat
vendored
21
gradlew.bat
vendored
@ -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
|
||||
|
@ -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
|
||||
|
BIN
motors/docs/C885T0002-TN-C-885.PIMotionMaster-EN.pdf
Normal file
BIN
motors/docs/C885T0002-TN-C-885.PIMotionMaster-EN.pdf
Normal file
Binary file not shown.
@ -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
|
||||
// }
|
||||
|
||||
|
||||
}
|
@ -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}")
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user