Virtual device complete
This commit is contained in:
parent
e4705b8239
commit
8f9bae6462
@ -1,10 +1,11 @@
|
||||
plugins {
|
||||
kotlin("jvm") version "1.4.0" apply false
|
||||
kotlin("js") version "1.4.0" apply false
|
||||
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")
|
||||
ksciencePublish {
|
||||
githubProject = "dataforge-control"
|
||||
bintrayRepo = "dataforge"
|
||||
}
|
||||
|
||||
apiValidation {
|
||||
validationDisabled = true
|
||||
}
|
@ -14,7 +14,7 @@ public interface DeviceHub : Provider {
|
||||
|
||||
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) {
|
||||
return buildMap {
|
||||
fun putAll(prefix: Name, hub: DeviceHub) {
|
||||
|
@ -28,7 +28,7 @@ public class DeviceManager : AbstractPlugin(), DeviceHub {
|
||||
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> {
|
||||
override val tag: PluginTag = PluginTag("devices", group = PluginTag.DATAFORGE_GROUP)
|
||||
|
@ -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))
|
||||
}
|
@ -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<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 ->
|
||||
// Response is not downloaded here.
|
||||
val channel = response.receive<ByteReadChannel>()
|
||||
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)
|
||||
}
|
||||
|
@ -5,6 +5,10 @@ plugins {
|
||||
|
||||
//TODO to be moved to a separate project
|
||||
|
||||
kotlin{
|
||||
explicitApi = null
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(project(":dataforge-device-core"))
|
||||
}
|
||||
|
@ -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?"
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
@ -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()
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user