From 8f9bae64624a395cae9521b3716ce65aea859e71 Mon Sep 17 00:00:00 2001 From: Alexander Nozik Date: Sun, 4 Oct 2020 22:36:44 +0300 Subject: [PATCH] Virtual device complete --- build.gradle.kts | 21 +- .../hep/dataforge/control/api/DeviceHub.kt | 2 +- .../control/controllers/DeviceManager.kt | 2 +- .../dataforge/control/server/conversions.kt | 2 +- .../kotlin/ru/mipt/npm/io/sse/SseTest.kt | 10 +- motors/build.gradle.kts | 4 + .../PiMotionMasterVirtualDevice.kt | 210 ++++++++++++++++++ .../PiMotionMasterVirtualPort.kt | 45 ---- .../devices/pimotionmaster/piDebugServer.kt | 3 +- settings.gradle.kts | 5 +- 10 files changed, 242 insertions(+), 62 deletions(-) create mode 100644 motors/src/main/kotlin/ru/mipt/npm/devices/pimotionmaster/PiMotionMasterVirtualDevice.kt delete mode 100644 motors/src/main/kotlin/ru/mipt/npm/devices/pimotionmaster/PiMotionMasterVirtualPort.kt diff --git a/build.gradle.kts b/build.gradle.kts index 47117f3..c89c248 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,10 +1,11 @@ -plugins{ - kotlin("jvm") version "1.4.0" apply false - kotlin("js") version "1.4.0" apply false +plugins { + id("ru.mipt.npm.project") + kotlin("jvm") apply false + kotlin("js") apply false } -val dataforgeVersion: String by extra("0.1.9-dev-2") -val ktorVersion: String by extra("1.4.0") +val dataforgeVersion: String by extra("0.2.0-dev-3") +val ktorVersion: String by extra("1.4.1") allprojects { repositories { @@ -19,5 +20,11 @@ allprojects { version = "0.0.1" } -val githubProject by extra("dataforge-control") -val bintrayRepo by extra("dataforge") \ No newline at end of file +ksciencePublish { + githubProject = "dataforge-control" + bintrayRepo = "dataforge" +} + +apiValidation { + validationDisabled = true +} \ 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 2010fde..65ccd11 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 @@ -14,7 +14,7 @@ public interface DeviceHub : Provider { override val defaultChainTarget: String get() = Device.DEVICE_TARGET - override fun provideTop(target: String): Map { + override fun content(target: String): Map { if (target == Device.DEVICE_TARGET) { return buildMap { fun putAll(prefix: Name, hub: DeviceHub) { diff --git a/dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/controllers/DeviceManager.kt b/dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/controllers/DeviceManager.kt index f316382..a7b69d8 100644 --- a/dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/controllers/DeviceManager.kt +++ b/dataforge-device-core/src/commonMain/kotlin/hep/dataforge/control/controllers/DeviceManager.kt @@ -28,7 +28,7 @@ public class DeviceManager : AbstractPlugin(), DeviceHub { top[name] = device } - override fun provideTop(target: String): Map = super.provideTop(target) + override fun content(target: String): Map = super.content(target) public companion object : PluginFactory { override val tag: PluginTag = PluginTag("devices", group = PluginTag.DATAFORGE_GROUP) 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 4b3f369..552aa0a 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 @@ -39,5 +39,5 @@ public suspend fun ApplicationCall.respondMessage(builder: DeviceMessage.() -> U } public suspend fun ApplicationCall.respondFail(builder: DeviceMessage.() -> Unit) { - respondMessage(DeviceMessage.fail(null, builder)) + respondMessage(DeviceMessage.fail(null, block = builder)) } \ No newline at end of file diff --git a/ktor-sse/src/jvmTest/kotlin/ru/mipt/npm/io/sse/SseTest.kt b/ktor-sse/src/jvmTest/kotlin/ru/mipt/npm/io/sse/SseTest.kt index 5b8ef4d..49b24bf 100644 --- a/ktor-sse/src/jvmTest/kotlin/ru/mipt/npm/io/sse/SseTest.kt +++ b/ktor-sse/src/jvmTest/kotlin/ru/mipt/npm/io/sse/SseTest.kt @@ -17,12 +17,11 @@ import io.ktor.server.cio.CIO import io.ktor.server.engine.embeddedServer import io.ktor.util.KtorExperimentalAPI import io.ktor.utils.io.ByteReadChannel -import kotlinx.coroutines.delay +import kotlinx.coroutines.* import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.map -import kotlinx.coroutines.runBlocking import org.junit.jupiter.api.Test @OptIn(KtorExperimentalAPI::class) @@ -33,19 +32,20 @@ suspend fun ApplicationCall.respondSse(events: Flow) { } } -suspend fun HttpClient.readSse(address: String, block: suspend (SseEvent) -> Unit): Unit = +suspend fun HttpClient.readSse(address: String, block: suspend (SseEvent) -> Unit): Job = launch { get(address).execute { response: HttpResponse -> // Response is not downloaded here. val channel = response.receive() val flow = channel.readSseFlow() flow.collect(block) } +} class SseTest { @OptIn(KtorExperimentalAPI::class) @Test fun testSseIntegration() { - runBlocking { + runBlocking(Dispatchers.Default) { val server = embeddedServer(CIO, 12080) { routing { get("/") { @@ -67,6 +67,8 @@ class SseTest { client.readSse("http://localhost:12080") { println(it) } + delay(2000) + println("Closing the client after waiting") client.close() server.stop(1000, 1000) } diff --git a/motors/build.gradle.kts b/motors/build.gradle.kts index 4b747ef..23410d9 100644 --- a/motors/build.gradle.kts +++ b/motors/build.gradle.kts @@ -5,6 +5,10 @@ plugins { //TODO to be moved to a separate project +kotlin{ + explicitApi = null +} + dependencies { implementation(project(":dataforge-device-core")) } 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 new file mode 100644 index 0000000..695009e --- /dev/null +++ b/motors/src/main/kotlin/ru/mipt/npm/devices/pimotionmaster/PiMotionMasterVirtualDevice.kt @@ -0,0 +1,210 @@ +package ru.mipt.npm.devices.pimotionmaster + +import hep.dataforge.context.Context +import hep.dataforge.control.ports.AbstractPort +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.coroutines.launch +import kotlin.math.abs +import kotlin.time.Duration + +public abstract class VirtualDevice { + protected abstract val scope: CoroutineScope + + public abstract suspend fun evaluateRequest(request: ByteArray) + + private val toSend = Channel(100) + + public val responses: Flow get() = toSend.receiveAsFlow() + + protected suspend fun send(response: ByteArray) { + toSend.send(response) + } +// +// protected suspend fun respond(response: String){ +// respond(response.encodeToByteArray()) +// } + + protected fun respondInFuture(delay: Duration, response: suspend () -> ByteArray): Job = scope.launch { + delay(delay) + send(response()) + } +} + +public class VirtualPort(private val device: VirtualDevice, context: Context) : AbstractPort(context) { + + private val respondJob = scope.launch { + device.responses.collect { + receive(it) + } + } + + override suspend fun write(data: ByteArray) { + device.evaluateRequest(data) + } + + override fun close() { + respondJob.cancel() + super.close() + } +} + + +class PiMotionMasterVirtualDevice(override val scope: CoroutineScope, axisIds: List) : VirtualDevice() { + + init { + //add asynchronous send logic here + } + + private val axisID = "0" + + private var errorCode: Int = 0 + + private val axisState: Map = axisIds.associateWith { VirtualAxisState() } + + private inner class VirtualAxisState { + private var movementJob: Job? = null + + private fun startMovement() { + movementJob?.cancel() + movementJob = scope.launch { + while (!onTarget()) { + delay(100) + val proposedStep = velocity / 10 + val distance = targetPosition - position + when { + abs(distance) < proposedStep -> { + position = targetPosition + } + targetPosition > position -> { + position += proposedStep + } + else -> { + position -= proposedStep + } + } + } + } + } + + var referenceMode = 1 + + var velocity = 0.6 + + var position = 0.0 + private set + var servoMode: Int = 1 + + var targetPosition = 0.0 + set(value) { + field = value + if (servoMode == 1) { + startMovement() + } + } + + fun onTarget() = abs(targetPosition - position) < 0.001 + + val minPosition = 0.0 + val maxPosition = 26.0 + } + + + private fun respond(str: String) = scope.launch { + send((str + "\n").encodeToByteArray()) + } + + private fun respondForAllAxis(axisIds: List, extract: VirtualAxisState.(index: String) -> Any) { + val selectedAxis = if (axisIds.isEmpty()) { + axisState.keys + } else { + axisIds + } + val response = selectedAxis.joinToString(postfix = "\n", separator = " \n") { + axisState.getValue(it).extract(it).toString() + } + respond(response) + } + + override suspend fun evaluateRequest(request: ByteArray) { + assert(request.last() == '\n'.toByte()) + val string = request.decodeToString().substringBefore("\n") + val parts = string.split(' ') + val command = parts.firstOrNull() ?: error("Command not present") + + val axisIds: List = parts.drop(1) + + when (command) { + "XXX" -> respond("") + "IDN?" -> respond("DataForge-device demo") + "VER?" -> respond("test") + "HLP?" -> respond(""" + The following commands are valid: + #4 Request Status Register + #5 Request Motion Status + #7 Request Controller Ready Status + #24 Stop All Axes + *IDN? Get Device Identification + CST? [{}] Get Assignment Of Stages To Axes + CSV? Get Current Syntax Version + ERR? Get Error Number + FRF [{}] Fast Reference Move To Reference Switch + FRF? [{}] Get Referencing Result + HLP? Get List Of Available Commands + HLT [{}] Halt Motion Smoothly + IFC { } Set Interface Parameters Temporarily + IFC? [{}] Get Current Interface Parameters + IFS { } Set Interface Parameters As Default Values + IFS? [{}] Get Interface Parameters As Default Values + INI Initialize Axes + MAN? Get Help String For Command + MOV { } Set Target Position (start absolute motion) + MOV? [{}] Get Target Position + ONT? [{}] Get On-Target State + POS { } Set Real Position (does not cause motion) + POS? [{}] Get Real Position + RBT Reboot System + RON { } Set Reference Mode + RON? [{}] Get Reference Mode + SAI? [ALL] Get List Of Current Axis Identifiers + SRG? { } Query Status Register Value + STP Stop All Axes + SVO { } Set Servo Mode + SVO? [{}] Get Servo Mode + TMN? [{}] Get Minimum Commandable Position + TMX? [{}] Get Maximum Commandable Position + VEL { } Set Closed-Loop Velocity + VEL? [{}] Get Closed-Loop Velocity + VER? Get Versions Of Firmware And Drivers + end of help + """.trimIndent()) + "ERR?" -> respond(errorCode.toString()) + "SAI?" -> respondForAllAxis(axisIds) { it } + "CST?" -> respond(WAT) + "RON?" -> respondForAllAxis(axisIds) { referenceMode } + "FRF?" -> respondForAllAxis(axisIds) { "1" } // WAT? + "SVO?" -> respondForAllAxis(axisIds) { servoMode } + "MVO?" -> respondForAllAxis(axisIds) { targetPosition } + "POS?" -> respondForAllAxis(axisIds) { position } + "TMN?" -> respondForAllAxis(axisIds) { minPosition } + "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() + } + else -> errorCode = 2 // do not send anything. Ser error code + } + } + + companion object { + private const val WAT = "WAT?" + } +} diff --git a/motors/src/main/kotlin/ru/mipt/npm/devices/pimotionmaster/PiMotionMasterVirtualPort.kt b/motors/src/main/kotlin/ru/mipt/npm/devices/pimotionmaster/PiMotionMasterVirtualPort.kt deleted file mode 100644 index 7b13330..0000000 --- a/motors/src/main/kotlin/ru/mipt/npm/devices/pimotionmaster/PiMotionMasterVirtualPort.kt +++ /dev/null @@ -1,45 +0,0 @@ -package ru.mipt.npm.devices.pimotionmaster - -import hep.dataforge.context.Context -import hep.dataforge.control.ports.AbstractPort - -abstract class VirtualPort(context: Context) : AbstractPort(context) - - -class PiMotionMasterVirtualPort(context: Context) : VirtualPort(context) { - - init { - //add asynchronous send logic here - } - - private val axisID = "0" - - private var errorCode: Int = 0 - private var velocity: Float = 1.0f - private var position: Float = 0.0f - private var servoMode: Int = 1 - private var targetPosition: Float = 0.0f - - - private fun receive(str: String) = receive((str + "\n").toByteArray()) - - override suspend fun write(data: ByteArray) { - assert(data.last() == '\n'.toByte()) - val string = data.decodeToString().substringBefore("\n") - val parts = string.split(' ') - val command = parts.firstOrNull() ?: error("Command not present") - when (command) { - "XXX" -> receive("WAT?") - "VER?" -> receive("test") - "ERR?" -> receive(errorCode.toString()) - "SVO?" -> receive("$axisID=$servoMode") - "SVO" ->{ - val requestAxis = parts[1] - if(requestAxis == axisID) { - servoMode = parts[2].toInt() - } - } - else -> errorCode = 2 // do not send anything. Ser error code - } - } -} \ No newline at end of file diff --git a/motors/src/main/kotlin/ru/mipt/npm/devices/pimotionmaster/piDebugServer.kt b/motors/src/main/kotlin/ru/mipt/npm/devices/pimotionmaster/piDebugServer.kt index fd9fe2f..4d4306d 100644 --- a/motors/src/main/kotlin/ru/mipt/npm/devices/pimotionmaster/piDebugServer.kt +++ b/motors/src/main/kotlin/ru/mipt/npm/devices/pimotionmaster/piDebugServer.kt @@ -66,7 +66,8 @@ fun CoroutineScope.launchPiDebugServer(port: Int, virtualPort: Port): Job = laun fun main() { val port = 10024 - val virtualPort = PiMotionMasterVirtualPort(Global) + val virtualDevice = PiMotionMasterVirtualDevice(Global, listOf("1","2")) + val virtualPort = VirtualPort(virtualDevice, Global) runBlocking(Dispatchers.Default) { val serverJob = launchPiDebugServer(port, virtualPort) readLine() diff --git a/settings.gradle.kts b/settings.gradle.kts index ed10e46..e0ca653 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,6 +1,6 @@ pluginManagement { - val kotlinVersion = "1.4.10" - val toolsVersion = "0.6.0" + val kotlinVersion = "1.4.20-M1" + val toolsVersion = "0.6.3-dev-1.4.20-M1" repositories { mavenLocal() @@ -14,6 +14,7 @@ pluginManagement { } plugins { + id("ru.mipt.npm.project") version toolsVersion id("ru.mipt.npm.mpp") version toolsVersion id("ru.mipt.npm.jvm") version toolsVersion id("ru.mipt.npm.js") version toolsVersion