From 62dc6ef1277c6cfdfe784e929fb2ef30d019bbc9 Mon Sep 17 00:00:00 2001 From: Alexander Nozik Date: Sun, 11 Oct 2020 22:37:39 +0300 Subject: [PATCH] More or less working motors --- dataforge-device-core/build.gradle.kts | 9 +- .../hep/dataforge/control/ports/PortProxy.kt | 37 +++-- .../hep/dataforge/control/ports/phrases.kt | 11 +- .../control/controllers/delegates.kt | 2 +- .../hep/dataforge/control/ports/TcpPort.kt | 6 +- dataforge-device-server/build.gradle.kts | 2 +- .../dataforge/control/server/sseOnServer.kt | 4 +- .../build.gradle.kts | 4 +- .../hep/dataforge/control}/sse/SseEvent.kt | 2 +- .../dataforge/control/ports/KtorTcpPort.kt | 3 +- .../dataforge/control/ports/TcpPortTest.kt | 0 .../hep/dataforge/control}/sse/SseTest.kt | 2 +- dataforge-magix-client/build.gradle.kts | 2 +- .../dataforge/control/client/MagixClient.kt | 4 +- motors/build.gradle.kts | 2 +- .../pimotionmaster/PiMotionMasterApp.kt | 96 ++++++++++-- .../pimotionmaster/PiMotionMasterDevice.kt | 142 +++++++++++------- .../PiMotionMasterVirtualDevice.kt | 25 ++- .../pimotionmaster/fxDeviceProperties.kt | 56 +++++++ settings.gradle.kts | 4 +- 20 files changed, 290 insertions(+), 123 deletions(-) rename {ktor-sse => dataforge-device-tcp}/build.gradle.kts (81%) rename {ktor-sse/src/commonMain/kotlin/ru/mipt/npm/io => dataforge-device-tcp/src/commonMain/kotlin/hep/dataforge/control}/sse/SseEvent.kt (98%) rename {dataforge-device-core => dataforge-device-tcp}/src/jvmMain/kotlin/hep/dataforge/control/ports/KtorTcpPort.kt (96%) rename {dataforge-device-core => dataforge-device-tcp}/src/jvmTest/kotlin/hep/dataforge/control/ports/TcpPortTest.kt (100%) rename {ktor-sse/src/jvmTest/kotlin/ru/mipt/npm/io => dataforge-device-tcp/src/jvmTest/kotlin/hep/dataforge/control}/sse/SseTest.kt (98%) create mode 100644 motors/src/main/kotlin/ru/mipt/npm/devices/pimotionmaster/fxDeviceProperties.kt diff --git a/dataforge-device-core/build.gradle.kts b/dataforge-device-core/build.gradle.kts index 1a47028..55e897b 100644 --- a/dataforge-device-core/build.gradle.kts +++ b/dataforge-device-core/build.gradle.kts @@ -4,6 +4,7 @@ plugins { } val dataforgeVersion: String by rootProject.extra +val ktorVersion: String by rootProject.extra kscience { useCoroutines() @@ -17,13 +18,9 @@ kotlin { api("hep.dataforge:dataforge-io:$dataforgeVersion") } } - jvmMain{ - dependencies{ - api("io.ktor:ktor-network:1.3.2") - } - } - jsMain{ + jvmTest{ dependencies{ + api("io.ktor:ktor-network:$ktorVersion") } } } 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 index ee8c42d..df7dc17 100644 --- 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 @@ -5,9 +5,9 @@ import hep.dataforge.context.ContextAware import hep.dataforge.context.Global import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.channelFlow import kotlinx.coroutines.flow.collect -import kotlinx.coroutines.isActive +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock @@ -31,40 +31,37 @@ public class PortProxy(override val context: Context = Global, public val factor } } - /** - * Ensure that the port is open. If it is already open, does nothing. Otherwise, open a new port. - */ - public suspend fun open() { - port()//ignore result - } - override suspend fun send(data: ByteArray) { port().send(data) } @OptIn(ExperimentalCoroutinesApi::class) - override fun receiving(): Flow = channelFlow { - while (isActive) { + override fun receiving(): Flow = flow { + while (true) { try { - //recreate port and Flow on cancel + //recreate port and Flow on connection problems port().receiving().collect { - send(it) + emit(it) } } catch (t: Throwable) { logger.warn(t){"Port read failed. Reconnecting."} - //cancel -// if (t is CancellationException) { -// cancel(t) -// } + mutex.withLock { + actualPort?.close() + actualPort = null + } } } - }// port().receiving() + } // open by default override fun isOpen(): Boolean = true override fun close() { - actualPort?.close() - actualPort = null + context.launch { + mutex.withLock { + actualPort?.close() + actualPort = null + } + } } } \ No newline at end of file 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 2f21757..3559840 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 @@ -1,21 +1,20 @@ 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.coroutines.flow.transform import kotlinx.io.ByteArrayOutput /** - * Transform byte fragments into complete phrases using given delimiter + * Transform byte fragments into complete phrases using given delimiter. Not thread safe. */ -public fun Flow.withDelimiter(delimiter: ByteArray, expectedMessageSize: Int = 32): Flow = flow { +public fun Flow.withDelimiter(delimiter: ByteArray, expectedMessageSize: Int = 32): Flow { require(delimiter.isNotEmpty()) { "Delimiter must not be empty" } var output = ByteArrayOutput(expectedMessageSize) var matcherPosition = 0 - collect { chunk -> + return transform { chunk -> chunk.forEach { byte -> output.writeByte(byte) //matching current symbol in delimiter @@ -39,7 +38,7 @@ public fun Flow.withDelimiter(delimiter: ByteArray, expectedMessageSi * 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(),expectedMessageSize).map { it.decodeToString() } + return withDelimiter(delimiter.encodeToByteArray(), expectedMessageSize).map { it.decodeToString() } } /** diff --git a/dataforge-device-core/src/jvmMain/kotlin/hep/dataforge/control/controllers/delegates.kt b/dataforge-device-core/src/jvmMain/kotlin/hep/dataforge/control/controllers/delegates.kt index fb03f6d..4489972 100644 --- a/dataforge-device-core/src/jvmMain/kotlin/hep/dataforge/control/controllers/delegates.kt +++ b/dataforge-device-core/src/jvmMain/kotlin/hep/dataforge/control/controllers/delegates.kt @@ -34,7 +34,7 @@ public fun ReadOnlyDeviceProperty.convert( metaConverter: MetaConverter, forceRead: Boolean, ): ReadOnlyProperty { - return ReadOnlyProperty { thisRef, property -> + return ReadOnlyProperty { _, _ -> runBlocking(scope.coroutineContext) { read(forceRead).let { metaConverter.itemToObject(it) } } 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 39bab89..9a9ddea 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 @@ -62,7 +62,11 @@ public class TcpPort private constructor( override fun close() { listenerJob.cancel() - futureChannel.cancel() + if(futureChannel.isCompleted){ + futureChannel.getCompleted().close() + } else { + futureChannel.cancel() + } super.close() } diff --git a/dataforge-device-server/build.gradle.kts b/dataforge-device-server/build.gradle.kts index 3c1381a..b440609 100644 --- a/dataforge-device-server/build.gradle.kts +++ b/dataforge-device-server/build.gradle.kts @@ -11,8 +11,8 @@ val dataforgeVersion: String by rootProject.extra val ktorVersion: String by rootProject.extra dependencies{ - implementation(project(":ktor-sse")) implementation(project(":dataforge-device-core")) + implementation(project(":dataforge-device-tcp")) implementation("io.ktor:ktor-server-cio:$ktorVersion") implementation("io.ktor:ktor-websockets:$ktorVersion") implementation("io.ktor:ktor-serialization:$ktorVersion") diff --git a/dataforge-device-server/src/main/kotlin/hep/dataforge/control/server/sseOnServer.kt b/dataforge-device-server/src/main/kotlin/hep/dataforge/control/server/sseOnServer.kt index c99dd92..0fb30ae 100644 --- a/dataforge-device-server/src/main/kotlin/hep/dataforge/control/server/sseOnServer.kt +++ b/dataforge-device-server/src/main/kotlin/hep/dataforge/control/server/sseOnServer.kt @@ -1,5 +1,7 @@ package hep.dataforge.control.server +import hep.dataforge.control.sse.SseEvent +import hep.dataforge.control.sse.writeSseFlow import io.ktor.application.ApplicationCall import io.ktor.http.CacheControl import io.ktor.http.ContentType @@ -8,8 +10,6 @@ import io.ktor.response.respondBytesWriter import io.ktor.util.KtorExperimentalAPI import kotlinx.coroutines.channels.ReceiveChannel import kotlinx.coroutines.flow.Flow -import ru.mipt.npm.io.sse.SseEvent -import ru.mipt.npm.io.sse.writeSseFlow /** * Method that responds an [ApplicationCall] by reading all the [SseEvent]s from the specified [events] [ReceiveChannel] diff --git a/ktor-sse/build.gradle.kts b/dataforge-device-tcp/build.gradle.kts similarity index 81% rename from ktor-sse/build.gradle.kts rename to dataforge-device-tcp/build.gradle.kts index 2e5c1a6..0312ed2 100644 --- a/ktor-sse/build.gradle.kts +++ b/dataforge-device-tcp/build.gradle.kts @@ -2,7 +2,6 @@ plugins { id("ru.mipt.npm.mpp") } -group = "ru.mipt.npm" val ktorVersion: String by rootProject.extra @@ -14,7 +13,8 @@ kotlin { sourceSets { commonMain { dependencies { - api("io.ktor:ktor-io:$ktorVersion") + api(project(":dataforge-device-core")) + api("io.ktor:ktor-network:$ktorVersion") } } jvmTest{ diff --git a/ktor-sse/src/commonMain/kotlin/ru/mipt/npm/io/sse/SseEvent.kt b/dataforge-device-tcp/src/commonMain/kotlin/hep/dataforge/control/sse/SseEvent.kt similarity index 98% rename from ktor-sse/src/commonMain/kotlin/ru/mipt/npm/io/sse/SseEvent.kt rename to dataforge-device-tcp/src/commonMain/kotlin/hep/dataforge/control/sse/SseEvent.kt index f5136ea..3dd9f79 100644 --- a/ktor-sse/src/commonMain/kotlin/ru/mipt/npm/io/sse/SseEvent.kt +++ b/dataforge-device-tcp/src/commonMain/kotlin/hep/dataforge/control/sse/SseEvent.kt @@ -1,4 +1,4 @@ -package ru.mipt.npm.io.sse +package hep.dataforge.control.sse import io.ktor.utils.io.* import kotlinx.coroutines.ExperimentalCoroutinesApi diff --git a/dataforge-device-core/src/jvmMain/kotlin/hep/dataforge/control/ports/KtorTcpPort.kt b/dataforge-device-tcp/src/jvmMain/kotlin/hep/dataforge/control/ports/KtorTcpPort.kt similarity index 96% rename from dataforge-device-core/src/jvmMain/kotlin/hep/dataforge/control/ports/KtorTcpPort.kt rename to dataforge-device-tcp/src/jvmMain/kotlin/hep/dataforge/control/ports/KtorTcpPort.kt index de2ea85..523cbc9 100644 --- a/dataforge-device-core/src/jvmMain/kotlin/hep/dataforge/control/ports/KtorTcpPort.kt +++ b/dataforge-device-tcp/src/jvmMain/kotlin/hep/dataforge/control/ports/KtorTcpPort.kt @@ -11,6 +11,7 @@ import io.ktor.network.sockets.openReadChannel import io.ktor.network.sockets.openWriteChannel import io.ktor.util.KtorExperimentalAPI import io.ktor.utils.io.consumeEachBufferRange +import io.ktor.utils.io.core.Closeable import io.ktor.utils.io.writeAvailable import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async @@ -24,7 +25,7 @@ public class KtorTcpPort internal constructor( public val host: String, public val port: Int, coroutineContext: CoroutineContext = context.coroutineContext, -) : AbstractPort(context, coroutineContext), AutoCloseable { +) : AbstractPort(context, coroutineContext), Closeable { override fun toString(): String = "port[tcp:$host:$port]" diff --git a/dataforge-device-core/src/jvmTest/kotlin/hep/dataforge/control/ports/TcpPortTest.kt b/dataforge-device-tcp/src/jvmTest/kotlin/hep/dataforge/control/ports/TcpPortTest.kt similarity index 100% rename from dataforge-device-core/src/jvmTest/kotlin/hep/dataforge/control/ports/TcpPortTest.kt rename to dataforge-device-tcp/src/jvmTest/kotlin/hep/dataforge/control/ports/TcpPortTest.kt diff --git a/ktor-sse/src/jvmTest/kotlin/ru/mipt/npm/io/sse/SseTest.kt b/dataforge-device-tcp/src/jvmTest/kotlin/hep/dataforge/control/sse/SseTest.kt similarity index 98% rename from ktor-sse/src/jvmTest/kotlin/ru/mipt/npm/io/sse/SseTest.kt rename to dataforge-device-tcp/src/jvmTest/kotlin/hep/dataforge/control/sse/SseTest.kt index 49b24bf..86d6db0 100644 --- a/ktor-sse/src/jvmTest/kotlin/ru/mipt/npm/io/sse/SseTest.kt +++ b/dataforge-device-tcp/src/jvmTest/kotlin/hep/dataforge/control/sse/SseTest.kt @@ -1,4 +1,4 @@ -package ru.mipt.npm.io.sse +package hep.dataforge.control.sse import io.ktor.application.ApplicationCall import io.ktor.application.call diff --git a/dataforge-magix-client/build.gradle.kts b/dataforge-magix-client/build.gradle.kts index 107e787..679b062 100644 --- a/dataforge-magix-client/build.gradle.kts +++ b/dataforge-magix-client/build.gradle.kts @@ -10,7 +10,7 @@ kotlin { commonMain { dependencies { implementation(project(":dataforge-device-core")) - implementation(project(":ktor-sse")) + implementation(project(":dataforge-device-tcp")) implementation("io.ktor:ktor-client-core:$ktorVersion") } } diff --git a/dataforge-magix-client/src/commonMain/kotlin/hep/dataforge/control/client/MagixClient.kt b/dataforge-magix-client/src/commonMain/kotlin/hep/dataforge/control/client/MagixClient.kt index 926f26f..0a8af4a 100644 --- a/dataforge-magix-client/src/commonMain/kotlin/hep/dataforge/control/client/MagixClient.kt +++ b/dataforge-magix-client/src/commonMain/kotlin/hep/dataforge/control/client/MagixClient.kt @@ -3,6 +3,8 @@ package hep.dataforge.control.client import hep.dataforge.control.controllers.DeviceManager import hep.dataforge.control.controllers.DeviceMessage import hep.dataforge.control.controllers.respondMessage +import hep.dataforge.control.sse.SseEvent +import hep.dataforge.control.sse.readSseFlow import hep.dataforge.meta.toJson import hep.dataforge.meta.toMeta import hep.dataforge.meta.wrap @@ -21,8 +23,6 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.flow.collect import kotlinx.coroutines.launch import kotlinx.serialization.json.* -import ru.mipt.npm.io.sse.SseEvent -import ru.mipt.npm.io.sse.readSseFlow import kotlin.coroutines.CoroutineContext diff --git a/motors/build.gradle.kts b/motors/build.gradle.kts index f9b09c1..eef45a2 100644 --- a/motors/build.gradle.kts +++ b/motors/build.gradle.kts @@ -15,7 +15,7 @@ kotlin{ val ktorVersion: String by rootProject.extra dependencies { - implementation(project(":dataforge-device-core")) + implementation(project(":dataforge-device-tcp")) implementation(project(":dataforge-magix-client")) implementation("no.tornado:tornadofx:1.7.20") } diff --git a/motors/src/main/kotlin/ru/mipt/npm/devices/pimotionmaster/PiMotionMasterApp.kt b/motors/src/main/kotlin/ru/mipt/npm/devices/pimotionmaster/PiMotionMasterApp.kt index 578ffc1..68de8f2 100644 --- a/motors/src/main/kotlin/ru/mipt/npm/devices/pimotionmaster/PiMotionMasterApp.kt +++ b/motors/src/main/kotlin/ru/mipt/npm/devices/pimotionmaster/PiMotionMasterApp.kt @@ -3,10 +3,15 @@ package ru.mipt.npm.devices.pimotionmaster import hep.dataforge.context.Global import hep.dataforge.control.controllers.DeviceManager import hep.dataforge.control.controllers.installing -import javafx.beans.property.SimpleBooleanProperty +import javafx.beans.property.ReadOnlyProperty import javafx.beans.property.SimpleIntegerProperty +import javafx.beans.property.SimpleObjectProperty import javafx.beans.property.SimpleStringProperty +import javafx.collections.FXCollections import javafx.scene.Parent +import javafx.scene.layout.Priority +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch import tornadofx.* class PiMotionMasterApp : App(PiMotionMasterView::class) @@ -25,33 +30,98 @@ class PiMotionMasterController : Controller() { class PiMotionMasterView : View() { private val controller: PiMotionMasterController by inject() + val device = controller.motionMaster + + private val connectedProperty: ReadOnlyProperty = device.connected.fxProperty(device) + private val debugServerJobProperty = SimpleObjectProperty() + private val debugServerStarted = debugServerJobProperty.booleanBinding { it != null } + private val axisList = FXCollections.observableArrayList>() override val root: Parent = borderpane { top { form { val host = SimpleStringProperty("127.0.0.1") val port = SimpleIntegerProperty(10024) - val virtual = SimpleBooleanProperty(false) fieldset("Address:") { field("Host:") { - textfield(host){ - enableWhen(virtual.not()) + textfield(host) { + enableWhen(debugServerStarted.not()) } } field("Port:") { - textfield(port) - } - field("Virtual device:") { - checkbox(property = virtual) + textfield(port){ + stripNonNumeric() + } + button { + hgrow = Priority.ALWAYS + textProperty().bind(debugServerStarted.stringBinding { + if (it != true) { + "Start debug server" + } else { + "Stop debug server" + } + }) + action { + if (!debugServerStarted.get()) { + debugServerJobProperty.value = + controller.context.launchPiDebugServer(port.get(), listOf("1", "2")) + } else { + debugServerJobProperty.get().cancel() + debugServerJobProperty.value = null + } + } + } } } - button("Connect") { - action { - if(virtual.get()){ - controller.context.launchPiDebugServer(port.get(), listOf("1", "2")) + button { + hgrow = Priority.ALWAYS + textProperty().bind(connectedProperty.stringBinding { + if (it == false) { + "Connect" + } else { + "Disconnect" + } + }) + action { + if (!connectedProperty.value) { + device.connect(host.get(), port.get()) + axisList.addAll(device.axes.entries) + } else { + axisList.removeAll() + device.disconnect() + } + } + } + + + } + } + + center { + listview(axisList) { + cellFormat { (name, axis) -> + hbox { + minHeight = 40.0 + label(name) + controller.context.launch { + val min = axis.minPosition.readTyped(true) + val max = axis.maxPosition.readTyped(true) + runLater { + slider(min.toDouble()..max.toDouble()){ + hgrow = Priority.ALWAYS + valueProperty().onChange { + isDisable = true + launch { + axis.move(value) + runLater { + isDisable = false + } + } + } + } + } } - controller.motionMaster.connect(host.get(), port.get()) } } } 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 index 8358a0f..8ccd67a 100644 --- a/motors/src/main/kotlin/ru/mipt/npm/devices/pimotionmaster/PiMotionMasterDevice.kt +++ b/motors/src/main/kotlin/ru/mipt/npm/devices/pimotionmaster/PiMotionMasterDevice.kt @@ -22,19 +22,68 @@ import kotlin.time.Duration class PiMotionMasterDevice( context: Context, - private val portFactory: PortFactory = TcpPort, + private val portFactory: PortFactory = KtorTcpPort, ) : DeviceBase(context), DeviceHub { override val scope: CoroutineScope = CoroutineScope( context.coroutineContext + SupervisorJob(context.coroutineContext[Job]) ) - private var address: Meta? = null + private var port: Port? = null + //TODO make proxy work + //PortProxy { portFactory(address ?: error("The device is not connected"), context) } + val connected by readingBoolean(false, descriptorBuilder = { info = "True if the connection address is defined and the device is initialized" }) { - address != null + port != null + } + + + val connect: DeviceAction by acting({ + info = "Connect to specific port and initialize axis" + }) { portSpec -> + //Clear current actions if present + if (port != null) { + disconnect() + } + //Update port + //address = portSpec.node + port = portFactory(portSpec.node!!, context) + connected.updateLogical(true) +// connector.open() + //Initialize axes + if (portSpec != null) { + val idn = identity.read() + failIfError { "Can't connect to $portSpec. Error code: $it" } + logger.info { "Connected to $idn on $portSpec" } + val ids = request("SAI?").map { it.trim() } + if (ids != axes.keys.toList()) { + //re-define axes if needed + axes = ids.associateWith { Axis(it) } + } + ids.map { it.asValue() }.asValue().asMetaItem() + initialize() + failIfError() + } + } + + val disconnect: DeviceAction by acting({ + info = "Disconnect the program from the device if it is connected" + }) { + if (port != null) { + stop() + port?.close() + } + port = null + connected.updateLogical(false) + } + + fun disconnect() { + runBlocking{ + disconnect.invoke() + } } val timeout: DeviceProperty by writingVirtual(200.asValue()) { @@ -43,8 +92,6 @@ class PiMotionMasterDevice( var timeoutValue: Duration by timeout.duration() - private val connector = PortProxy { portFactory(address ?: error("The device is not connected"), context) } - /** * Name-friendly accessor for axis */ @@ -58,46 +105,8 @@ class PiMotionMasterDevice( if (errorCode != 0) error(message(errorCode)) } - val connect: DeviceAction by acting({ - info = "Connect to specific port and initialize axis" - }) { portSpec -> - //Clear current actions if present - if (address != null) { - stop() - } - //Update port - address = portSpec.node - connected.invalidate() - connector.open() - //Initialize axes - if (portSpec != null) { - val idn = identity.read() - failIfError { "Can't connect to $portSpec. Error code: $it" } - logger.info { "Connected to $idn on $portSpec" } - val ids = request("SAI?") - if (ids != axes.keys.toList()) { - //re-define axes if needed - axes = ids.associateWith { Axis(it) } - } - ids.map { it.asValue() }.asValue().asMetaItem() - initialize() - failIfError() - } - } - - val disconnect: DeviceAction by acting({ - info = "Disconnect the program from the device if it is connected" - }) { - connector.close() - if (address != null) { - stop() - } - address = null - connected.invalidate() - } - fun connect(host: String, port: Int) { - scope.launch { + runBlocking { connect(Meta { "host" put host "port" put port @@ -119,14 +128,14 @@ class PiMotionMasterDevice( arguments.joinToString(prefix = " ", separator = " ", postfix = "") } val stringToSend = "$command$joinedArguments\n" - connector.send(stringToSend) + port?.send(stringToSend) ?: error("Not connected to device") } suspend fun getErrorCode(): Int = mutex.withLock { withTimeout(timeoutValue) { sendCommandInternal("ERR?") - val errorString = connector.receiving().withDelimiter("\n").first() - errorString.toInt() + val errorString = port?.receiving()?.withDelimiter("\n")?.first() ?: error("Not connected to device") + errorString.trim().toInt() } } @@ -137,17 +146,12 @@ class PiMotionMasterDevice( try { withTimeout(timeoutValue) { sendCommandInternal(command, *arguments) - val phrases = connector.receiving().withDelimiter("\n") - var lastLineFlag = false - phrases.transformWhile { line -> - if (lastLineFlag) { - false - } else { - emit(line) - lastLineFlag = !line.endsWith(" \n") - true - } + val phrases = port?.receiving()?.withDelimiter("\n") ?: error("Not connected to device") + val list = phrases.transformWhile { line -> + emit(line) + line.endsWith(" \n") }.toList() + list } } catch (ex: Throwable) { logger.warn { "Error during PIMotionMaster request. Requesting error code." } @@ -273,6 +277,26 @@ class PiMotionMasterDevice( send("FRF", axisId) } + val minPosition by readingNumber( + descriptorBuilder = { + info = "Minimal position value for the axis" + }, + getter = { + requestAndParse("TMN?", axisId)[axisId]?.toDoubleOrNull() + ?: error("Malformed `TMN?` response. Should include float value for $axisId") + } + ) + + val maxPosition by readingNumber( + descriptorBuilder = { + info = "Maximal position value for the axis" + }, + getter = { + requestAndParse("TMX?", axisId)[axisId]?.toDoubleOrNull() + ?: error("Malformed `TMX?` response. Should include float value for $axisId") + } + ) + val position: TypedDeviceProperty by axisNumberProperty("POS") { info = "The current axis position." } @@ -303,6 +327,10 @@ class PiMotionMasterDevice( delay(200) } } + + suspend fun move(target: Double) { + move(target.asMetaItem()) + } } companion object : DeviceFactory { diff --git a/motors/src/main/kotlin/ru/mipt/npm/devices/pimotionmaster/PiMotionMasterVirtualDevice.kt b/motors/src/main/kotlin/ru/mipt/npm/devices/pimotionmaster/PiMotionMasterVirtualDevice.kt index 384609c..4c17a6d 100644 --- a/motors/src/main/kotlin/ru/mipt/npm/devices/pimotionmaster/PiMotionMasterVirtualDevice.kt +++ b/motors/src/main/kotlin/ru/mipt/npm/devices/pimotionmaster/PiMotionMasterVirtualDevice.kt @@ -148,6 +148,14 @@ class PiMotionMasterVirtualDevice( respond(response) } + private suspend fun doForEachAxis(parts: List, action: suspend (key: String, value: String) -> Unit) { + var i = 0 + while (parts.size > 2 * i + 1) { + action(parts[2 * i + 1], parts[2 * i + 2]) + i++ + } + } + override suspend fun evaluateRequest(request: ByteArray) { assert(request.last() == '\n'.toByte()) val string = request.decodeToString().substringBefore("\n") @@ -222,7 +230,7 @@ class PiMotionMasterVirtualDevice( respond(errorCode.toString()) errorCode = 0 } - "SAI?" -> respond(axisIds.joinToString(separator = " \n")) + "SAI?" -> respond(axisState.keys.joinToString(separator = " \n")) "CST?" -> respondForAllAxis(axisIds) { "L-220.20SG" } "RON?" -> respondForAllAxis(axisIds) { referenceMode } "FRF?" -> respondForAllAxis(axisIds) { "1" } // WAT? @@ -233,10 +241,17 @@ class PiMotionMasterVirtualDevice( "TMX?" -> respondForAllAxis(axisIds) { maxPosition } "VEL?" -> respondForAllAxis(axisIds) { velocity } "SRG?" -> respond(WAT) - "SVO" -> { - val requestAxis = parts[1] - val servoMode = parts.last() - axisState[requestAxis]?.servoMode = servoMode.toInt() + "SVO" -> doForEachAxis(parts) { key, value -> + axisState[key]?.servoMode = value.toInt() + } + "MOV" -> doForEachAxis(parts) { key, value -> + axisState[key]?.targetPosition = value.toDouble() + } + "VEL"-> doForEachAxis(parts){key, value -> + axisState[key]?.velocity = value.toDouble() + } + "INI" -> { + logger.info { "Axes initialized!" } } else -> { logger.warn { "Unknown command: $command in message ${String(request)}" } diff --git a/motors/src/main/kotlin/ru/mipt/npm/devices/pimotionmaster/fxDeviceProperties.kt b/motors/src/main/kotlin/ru/mipt/npm/devices/pimotionmaster/fxDeviceProperties.kt new file mode 100644 index 0000000..7af82e4 --- /dev/null +++ b/motors/src/main/kotlin/ru/mipt/npm/devices/pimotionmaster/fxDeviceProperties.kt @@ -0,0 +1,56 @@ +package ru.mipt.npm.devices.pimotionmaster + +import hep.dataforge.control.api.Device +import hep.dataforge.control.base.TypedDeviceProperty +import hep.dataforge.control.base.TypedReadOnlyDeviceProperty +import javafx.beans.property.* +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import tornadofx.* + +fun TypedReadOnlyDeviceProperty.fxProperty(ownerDevice: Device?): ReadOnlyProperty = + object : ObjectPropertyBase() { + override fun getBean(): Any? = ownerDevice + override fun getName(): String = this@fxProperty.name + + init { + //Read incoming changes + flowTyped().onEach { + if (it != null) { + runLater { + set(it) + } + } else { + invalidated() + } + }.catch { + ownerDevice?.logger?.info { "Failed to set property $name to $it" } + }.launchIn(scope) + } + } + +fun TypedDeviceProperty.fxProperty(ownerDevice: Device?): Property = + object : ObjectPropertyBase() { + override fun getBean(): Any? = ownerDevice + override fun getName(): String = this@fxProperty.name + + init { + //Read incoming changes + flowTyped().onEach { + if (it != null) { + runLater { + set(it) + } + } else { + invalidated() + } + }.catch { + ownerDevice?.logger?.info { "Failed to set property $name to $it" } + }.launchIn(scope) + + onChange { + typedValue = it + } + } + } diff --git a/settings.gradle.kts b/settings.gradle.kts index 43c69c1..ad4e337 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -3,7 +3,7 @@ pluginManagement { val toolsVersion = "0.6.3-dev-1.4.20-M1" repositories { - mavenLocal() + //mavenLocal() jcenter() gradlePluginPortal() maven("https://kotlin.bintray.com/kotlinx") @@ -27,8 +27,8 @@ pluginManagement { rootProject.name = "dataforge-control" include( - ":ktor-sse", ":dataforge-device-core", + ":dataforge-device-tcp", ":dataforge-device-serial", ":dataforge-device-server", ":dataforge-magix-client",