Virtual device complete
This commit is contained in:
parent
e4705b8239
commit
8f9bae6462
@ -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
|
||||||
|
}
|
@ -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) {
|
||||||
|
@ -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)
|
||||||
|
@ -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))
|
||||||
}
|
}
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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"))
|
||||||
}
|
}
|
||||||
|
@ -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() {
|
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()
|
||||||
|
@ -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
|
||||||
|
Loading…
Reference in New Issue
Block a user