Virtual device complete

This commit is contained in:
Alexander Nozik 2020-10-04 22:36:44 +03:00
parent e4705b8239
commit 8f9bae6462
10 changed files with 242 additions and 62 deletions

View File

@ -1,10 +1,11 @@
plugins { plugins {
kotlin("jvm") version "1.4.0" apply false id("ru.mipt.npm.project")
kotlin("js") version "1.4.0" apply false kotlin("jvm") apply false
kotlin("js") apply false
} }
val dataforgeVersion: String by extra("0.1.9-dev-2") val dataforgeVersion: String by extra("0.2.0-dev-3")
val ktorVersion: String by extra("1.4.0") val ktorVersion: String by extra("1.4.1")
allprojects { allprojects {
repositories { repositories {
@ -19,5 +20,11 @@ allprojects {
version = "0.0.1" version = "0.0.1"
} }
val githubProject by extra("dataforge-control") ksciencePublish {
val bintrayRepo by extra("dataforge") githubProject = "dataforge-control"
bintrayRepo = "dataforge"
}
apiValidation {
validationDisabled = true
}

View File

@ -14,7 +14,7 @@ public interface DeviceHub : Provider {
override val defaultChainTarget: String get() = Device.DEVICE_TARGET override val defaultChainTarget: String get() = Device.DEVICE_TARGET
override fun provideTop(target: String): Map<Name, Any> { override fun content(target: String): Map<Name, Any> {
if (target == Device.DEVICE_TARGET) { if (target == Device.DEVICE_TARGET) {
return buildMap { return buildMap {
fun putAll(prefix: Name, hub: DeviceHub) { fun putAll(prefix: Name, hub: DeviceHub) {

View File

@ -28,7 +28,7 @@ public class DeviceManager : AbstractPlugin(), DeviceHub {
top[name] = device top[name] = device
} }
override fun provideTop(target: String): Map<Name, Any> = super<DeviceHub>.provideTop(target) override fun content(target: String): Map<Name, Any> = super<DeviceHub>.content(target)
public companion object : PluginFactory<DeviceManager> { public companion object : PluginFactory<DeviceManager> {
override val tag: PluginTag = PluginTag("devices", group = PluginTag.DATAFORGE_GROUP) override val tag: PluginTag = PluginTag("devices", group = PluginTag.DATAFORGE_GROUP)

View File

@ -39,5 +39,5 @@ public suspend fun ApplicationCall.respondMessage(builder: DeviceMessage.() -> U
} }
public suspend fun ApplicationCall.respondFail(builder: DeviceMessage.() -> Unit) { public suspend fun ApplicationCall.respondFail(builder: DeviceMessage.() -> Unit) {
respondMessage(DeviceMessage.fail(null, builder)) respondMessage(DeviceMessage.fail(null, block = builder))
} }

View File

@ -17,12 +17,11 @@ import io.ktor.server.cio.CIO
import io.ktor.server.engine.embeddedServer import io.ktor.server.engine.embeddedServer
import io.ktor.util.KtorExperimentalAPI import io.ktor.util.KtorExperimentalAPI
import io.ktor.utils.io.ByteReadChannel import io.ktor.utils.io.ByteReadChannel
import kotlinx.coroutines.delay import kotlinx.coroutines.*
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.runBlocking
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
@OptIn(KtorExperimentalAPI::class) @OptIn(KtorExperimentalAPI::class)
@ -33,19 +32,20 @@ suspend fun ApplicationCall.respondSse(events: Flow<SseEvent>) {
} }
} }
suspend fun HttpClient.readSse(address: String, block: suspend (SseEvent) -> Unit): Unit = suspend fun HttpClient.readSse(address: String, block: suspend (SseEvent) -> Unit): Job = launch {
get<HttpStatement>(address).execute { response: HttpResponse -> get<HttpStatement>(address).execute { response: HttpResponse ->
// Response is not downloaded here. // Response is not downloaded here.
val channel = response.receive<ByteReadChannel>() val channel = response.receive<ByteReadChannel>()
val flow = channel.readSseFlow() val flow = channel.readSseFlow()
flow.collect(block) flow.collect(block)
} }
}
class SseTest { class SseTest {
@OptIn(KtorExperimentalAPI::class) @OptIn(KtorExperimentalAPI::class)
@Test @Test
fun testSseIntegration() { fun testSseIntegration() {
runBlocking { runBlocking(Dispatchers.Default) {
val server = embeddedServer(CIO, 12080) { val server = embeddedServer(CIO, 12080) {
routing { routing {
get("/") { get("/") {
@ -67,6 +67,8 @@ class SseTest {
client.readSse("http://localhost:12080") { client.readSse("http://localhost:12080") {
println(it) println(it)
} }
delay(2000)
println("Closing the client after waiting")
client.close() client.close()
server.stop(1000, 1000) server.stop(1000, 1000)
} }

View File

@ -5,6 +5,10 @@ plugins {
//TODO to be moved to a separate project //TODO to be moved to a separate project
kotlin{
explicitApi = null
}
dependencies { dependencies {
implementation(project(":dataforge-device-core")) implementation(project(":dataforge-device-core"))
} }

View File

@ -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<ByteArray>(100)
public val responses: Flow<ByteArray> 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<String>) : VirtualDevice() {
init {
//add asynchronous send logic here
}
private val axisID = "0"
private var errorCode: Int = 0
private val axisState: Map<String, VirtualAxisState> = 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<String>, 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<String> = 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? [{<AxisID>}] Get Assignment Of Stages To Axes
CSV? Get Current Syntax Version
ERR? Get Error Number
FRF [{<AxisID>}] Fast Reference Move To Reference Switch
FRF? [{<AxisID>}] Get Referencing Result
HLP? Get List Of Available Commands
HLT [{<AxisID>}] Halt Motion Smoothly
IFC {<InterfacePam> <PamValue>} Set Interface Parameters Temporarily
IFC? [{<InterfacePam>}] Get Current Interface Parameters
IFS <Pswd> {<InterfacePam> <PamValue>} Set Interface Parameters As Default Values
IFS? [{<InterfacePam>}] Get Interface Parameters As Default Values
INI Initialize Axes
MAN? <CMD> Get Help String For Command
MOV {<AxisID> <Position>} Set Target Position (start absolute motion)
MOV? [{<AxisID>}] Get Target Position
ONT? [{<AxisID>}] Get On-Target State
POS {<AxisID> <Position>} Set Real Position (does not cause motion)
POS? [{<AxisID>}] Get Real Position
RBT Reboot System
RON {<AxisID> <ReferenceOn>} Set Reference Mode
RON? [{<AxisID>}] Get Reference Mode
SAI? [ALL] Get List Of Current Axis Identifiers
SRG? {<AxisID> <RegisterID>} Query Status Register Value
STP Stop All Axes
SVO {<AxisID> <ServoState>} Set Servo Mode
SVO? [{<AxisID>}] Get Servo Mode
TMN? [{<AxisID>}] Get Minimum Commandable Position
TMX? [{<AxisID>}] Get Maximum Commandable Position
VEL {<AxisID> <Velocity>} Set Closed-Loop Velocity
VEL? [{<AxisID>}] 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?"
}
}

View File

@ -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
}
}
}

View File

@ -66,7 +66,8 @@ fun CoroutineScope.launchPiDebugServer(port: Int, virtualPort: Port): Job = laun
fun main() { fun main() {
val port = 10024 val port = 10024
val virtualPort = PiMotionMasterVirtualPort(Global) val virtualDevice = PiMotionMasterVirtualDevice(Global, listOf("1","2"))
val virtualPort = VirtualPort(virtualDevice, Global)
runBlocking(Dispatchers.Default) { runBlocking(Dispatchers.Default) {
val serverJob = launchPiDebugServer(port, virtualPort) val serverJob = launchPiDebugServer(port, virtualPort)
readLine() readLine()

View File

@ -1,6 +1,6 @@
pluginManagement { pluginManagement {
val kotlinVersion = "1.4.10" val kotlinVersion = "1.4.20-M1"
val toolsVersion = "0.6.0" val toolsVersion = "0.6.3-dev-1.4.20-M1"
repositories { repositories {
mavenLocal() mavenLocal()
@ -14,6 +14,7 @@ pluginManagement {
} }
plugins { plugins {
id("ru.mipt.npm.project") version toolsVersion
id("ru.mipt.npm.mpp") version toolsVersion id("ru.mipt.npm.mpp") version toolsVersion
id("ru.mipt.npm.jvm") version toolsVersion id("ru.mipt.npm.jvm") version toolsVersion
id("ru.mipt.npm.js") version toolsVersion id("ru.mipt.npm.js") version toolsVersion