More or less working motors
This commit is contained in:
parent
23c66d9703
commit
62dc6ef127
@ -4,6 +4,7 @@ plugins {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val dataforgeVersion: String by rootProject.extra
|
val dataforgeVersion: String by rootProject.extra
|
||||||
|
val ktorVersion: String by rootProject.extra
|
||||||
|
|
||||||
kscience {
|
kscience {
|
||||||
useCoroutines()
|
useCoroutines()
|
||||||
@ -17,13 +18,9 @@ kotlin {
|
|||||||
api("hep.dataforge:dataforge-io:$dataforgeVersion")
|
api("hep.dataforge:dataforge-io:$dataforgeVersion")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
jvmMain{
|
jvmTest{
|
||||||
dependencies{
|
|
||||||
api("io.ktor:ktor-network:1.3.2")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
jsMain{
|
|
||||||
dependencies{
|
dependencies{
|
||||||
|
api("io.ktor:ktor-network:$ktorVersion")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,9 +5,9 @@ import hep.dataforge.context.ContextAware
|
|||||||
import hep.dataforge.context.Global
|
import hep.dataforge.context.Global
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.channelFlow
|
|
||||||
import kotlinx.coroutines.flow.collect
|
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.Mutex
|
||||||
import kotlinx.coroutines.sync.withLock
|
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) {
|
override suspend fun send(data: ByteArray) {
|
||||||
port().send(data)
|
port().send(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
override fun receiving(): Flow<ByteArray> = channelFlow {
|
override fun receiving(): Flow<ByteArray> = flow {
|
||||||
while (isActive) {
|
while (true) {
|
||||||
try {
|
try {
|
||||||
//recreate port and Flow on cancel
|
//recreate port and Flow on connection problems
|
||||||
port().receiving().collect {
|
port().receiving().collect {
|
||||||
send(it)
|
emit(it)
|
||||||
}
|
}
|
||||||
} catch (t: Throwable) {
|
} catch (t: Throwable) {
|
||||||
logger.warn(t){"Port read failed. Reconnecting."}
|
logger.warn(t){"Port read failed. Reconnecting."}
|
||||||
//cancel
|
mutex.withLock {
|
||||||
// if (t is CancellationException) {
|
actualPort?.close()
|
||||||
// cancel(t)
|
actualPort = null
|
||||||
// }
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}// port().receiving()
|
}
|
||||||
|
|
||||||
// open by default
|
// open by default
|
||||||
override fun isOpen(): Boolean = true
|
override fun isOpen(): Boolean = true
|
||||||
|
|
||||||
override fun close() {
|
override fun close() {
|
||||||
actualPort?.close()
|
context.launch {
|
||||||
actualPort = null
|
mutex.withLock {
|
||||||
|
actualPort?.close()
|
||||||
|
actualPort = null
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,21 +1,20 @@
|
|||||||
package hep.dataforge.control.ports
|
package hep.dataforge.control.ports
|
||||||
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.collect
|
|
||||||
import kotlinx.coroutines.flow.flow
|
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.coroutines.flow.transform
|
||||||
import kotlinx.io.ByteArrayOutput
|
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<ByteArray>.withDelimiter(delimiter: ByteArray, expectedMessageSize: Int = 32): Flow<ByteArray> = flow {
|
public fun Flow<ByteArray>.withDelimiter(delimiter: ByteArray, expectedMessageSize: Int = 32): Flow<ByteArray> {
|
||||||
require(delimiter.isNotEmpty()) { "Delimiter must not be empty" }
|
require(delimiter.isNotEmpty()) { "Delimiter must not be empty" }
|
||||||
|
|
||||||
var output = ByteArrayOutput(expectedMessageSize)
|
var output = ByteArrayOutput(expectedMessageSize)
|
||||||
var matcherPosition = 0
|
var matcherPosition = 0
|
||||||
|
|
||||||
collect { chunk ->
|
return transform { chunk ->
|
||||||
chunk.forEach { byte ->
|
chunk.forEach { byte ->
|
||||||
output.writeByte(byte)
|
output.writeByte(byte)
|
||||||
//matching current symbol in delimiter
|
//matching current symbol in delimiter
|
||||||
@ -39,7 +38,7 @@ public fun Flow<ByteArray>.withDelimiter(delimiter: ByteArray, expectedMessageSi
|
|||||||
* Transform byte fragments into utf-8 phrases using utf-8 delimiter
|
* Transform byte fragments into utf-8 phrases using utf-8 delimiter
|
||||||
*/
|
*/
|
||||||
public fun Flow<ByteArray>.withDelimiter(delimiter: String, expectedMessageSize: Int = 32): Flow<String> {
|
public fun Flow<ByteArray>.withDelimiter(delimiter: String, expectedMessageSize: Int = 32): Flow<String> {
|
||||||
return withDelimiter(delimiter.encodeToByteArray(),expectedMessageSize).map { it.decodeToString() }
|
return withDelimiter(delimiter.encodeToByteArray(), expectedMessageSize).map { it.decodeToString() }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -34,7 +34,7 @@ public fun <T : Any> ReadOnlyDeviceProperty.convert(
|
|||||||
metaConverter: MetaConverter<T>,
|
metaConverter: MetaConverter<T>,
|
||||||
forceRead: Boolean,
|
forceRead: Boolean,
|
||||||
): ReadOnlyProperty<Any?, T> {
|
): ReadOnlyProperty<Any?, T> {
|
||||||
return ReadOnlyProperty { thisRef, property ->
|
return ReadOnlyProperty { _, _ ->
|
||||||
runBlocking(scope.coroutineContext) {
|
runBlocking(scope.coroutineContext) {
|
||||||
read(forceRead).let { metaConverter.itemToObject(it) }
|
read(forceRead).let { metaConverter.itemToObject(it) }
|
||||||
}
|
}
|
||||||
|
@ -62,7 +62,11 @@ public class TcpPort private constructor(
|
|||||||
|
|
||||||
override fun close() {
|
override fun close() {
|
||||||
listenerJob.cancel()
|
listenerJob.cancel()
|
||||||
futureChannel.cancel()
|
if(futureChannel.isCompleted){
|
||||||
|
futureChannel.getCompleted().close()
|
||||||
|
} else {
|
||||||
|
futureChannel.cancel()
|
||||||
|
}
|
||||||
super.close()
|
super.close()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -11,8 +11,8 @@ val dataforgeVersion: String by rootProject.extra
|
|||||||
val ktorVersion: String by rootProject.extra
|
val ktorVersion: String by rootProject.extra
|
||||||
|
|
||||||
dependencies{
|
dependencies{
|
||||||
implementation(project(":ktor-sse"))
|
|
||||||
implementation(project(":dataforge-device-core"))
|
implementation(project(":dataforge-device-core"))
|
||||||
|
implementation(project(":dataforge-device-tcp"))
|
||||||
implementation("io.ktor:ktor-server-cio:$ktorVersion")
|
implementation("io.ktor:ktor-server-cio:$ktorVersion")
|
||||||
implementation("io.ktor:ktor-websockets:$ktorVersion")
|
implementation("io.ktor:ktor-websockets:$ktorVersion")
|
||||||
implementation("io.ktor:ktor-serialization:$ktorVersion")
|
implementation("io.ktor:ktor-serialization:$ktorVersion")
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
package hep.dataforge.control.server
|
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.application.ApplicationCall
|
||||||
import io.ktor.http.CacheControl
|
import io.ktor.http.CacheControl
|
||||||
import io.ktor.http.ContentType
|
import io.ktor.http.ContentType
|
||||||
@ -8,8 +10,6 @@ import io.ktor.response.respondBytesWriter
|
|||||||
import io.ktor.util.KtorExperimentalAPI
|
import io.ktor.util.KtorExperimentalAPI
|
||||||
import kotlinx.coroutines.channels.ReceiveChannel
|
import kotlinx.coroutines.channels.ReceiveChannel
|
||||||
import kotlinx.coroutines.flow.Flow
|
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]
|
* Method that responds an [ApplicationCall] by reading all the [SseEvent]s from the specified [events] [ReceiveChannel]
|
||||||
|
@ -2,7 +2,6 @@ plugins {
|
|||||||
id("ru.mipt.npm.mpp")
|
id("ru.mipt.npm.mpp")
|
||||||
}
|
}
|
||||||
|
|
||||||
group = "ru.mipt.npm"
|
|
||||||
|
|
||||||
val ktorVersion: String by rootProject.extra
|
val ktorVersion: String by rootProject.extra
|
||||||
|
|
||||||
@ -14,7 +13,8 @@ kotlin {
|
|||||||
sourceSets {
|
sourceSets {
|
||||||
commonMain {
|
commonMain {
|
||||||
dependencies {
|
dependencies {
|
||||||
api("io.ktor:ktor-io:$ktorVersion")
|
api(project(":dataforge-device-core"))
|
||||||
|
api("io.ktor:ktor-network:$ktorVersion")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
jvmTest{
|
jvmTest{
|
@ -1,4 +1,4 @@
|
|||||||
package ru.mipt.npm.io.sse
|
package hep.dataforge.control.sse
|
||||||
|
|
||||||
import io.ktor.utils.io.*
|
import io.ktor.utils.io.*
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
@ -11,6 +11,7 @@ import io.ktor.network.sockets.openReadChannel
|
|||||||
import io.ktor.network.sockets.openWriteChannel
|
import io.ktor.network.sockets.openWriteChannel
|
||||||
import io.ktor.util.KtorExperimentalAPI
|
import io.ktor.util.KtorExperimentalAPI
|
||||||
import io.ktor.utils.io.consumeEachBufferRange
|
import io.ktor.utils.io.consumeEachBufferRange
|
||||||
|
import io.ktor.utils.io.core.Closeable
|
||||||
import io.ktor.utils.io.writeAvailable
|
import io.ktor.utils.io.writeAvailable
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.async
|
import kotlinx.coroutines.async
|
||||||
@ -24,7 +25,7 @@ public class KtorTcpPort internal constructor(
|
|||||||
public val host: String,
|
public val host: String,
|
||||||
public val port: Int,
|
public val port: Int,
|
||||||
coroutineContext: CoroutineContext = context.coroutineContext,
|
coroutineContext: CoroutineContext = context.coroutineContext,
|
||||||
) : AbstractPort(context, coroutineContext), AutoCloseable {
|
) : AbstractPort(context, coroutineContext), Closeable {
|
||||||
|
|
||||||
override fun toString(): String = "port[tcp:$host:$port]"
|
override fun toString(): String = "port[tcp:$host:$port]"
|
||||||
|
|
@ -1,4 +1,4 @@
|
|||||||
package ru.mipt.npm.io.sse
|
package hep.dataforge.control.sse
|
||||||
|
|
||||||
import io.ktor.application.ApplicationCall
|
import io.ktor.application.ApplicationCall
|
||||||
import io.ktor.application.call
|
import io.ktor.application.call
|
@ -10,7 +10,7 @@ kotlin {
|
|||||||
commonMain {
|
commonMain {
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(project(":dataforge-device-core"))
|
implementation(project(":dataforge-device-core"))
|
||||||
implementation(project(":ktor-sse"))
|
implementation(project(":dataforge-device-tcp"))
|
||||||
implementation("io.ktor:ktor-client-core:$ktorVersion")
|
implementation("io.ktor:ktor-client-core:$ktorVersion")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,8 @@ package hep.dataforge.control.client
|
|||||||
import hep.dataforge.control.controllers.DeviceManager
|
import hep.dataforge.control.controllers.DeviceManager
|
||||||
import hep.dataforge.control.controllers.DeviceMessage
|
import hep.dataforge.control.controllers.DeviceMessage
|
||||||
import hep.dataforge.control.controllers.respondMessage
|
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.toJson
|
||||||
import hep.dataforge.meta.toMeta
|
import hep.dataforge.meta.toMeta
|
||||||
import hep.dataforge.meta.wrap
|
import hep.dataforge.meta.wrap
|
||||||
@ -21,8 +23,6 @@ import kotlinx.coroutines.Job
|
|||||||
import kotlinx.coroutines.flow.collect
|
import kotlinx.coroutines.flow.collect
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.serialization.json.*
|
import kotlinx.serialization.json.*
|
||||||
import ru.mipt.npm.io.sse.SseEvent
|
|
||||||
import ru.mipt.npm.io.sse.readSseFlow
|
|
||||||
import kotlin.coroutines.CoroutineContext
|
import kotlin.coroutines.CoroutineContext
|
||||||
|
|
||||||
|
|
||||||
|
@ -15,7 +15,7 @@ kotlin{
|
|||||||
val ktorVersion: String by rootProject.extra
|
val ktorVersion: String by rootProject.extra
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(project(":dataforge-device-core"))
|
implementation(project(":dataforge-device-tcp"))
|
||||||
implementation(project(":dataforge-magix-client"))
|
implementation(project(":dataforge-magix-client"))
|
||||||
implementation("no.tornado:tornadofx:1.7.20")
|
implementation("no.tornado:tornadofx:1.7.20")
|
||||||
}
|
}
|
||||||
|
@ -3,10 +3,15 @@ package ru.mipt.npm.devices.pimotionmaster
|
|||||||
import hep.dataforge.context.Global
|
import hep.dataforge.context.Global
|
||||||
import hep.dataforge.control.controllers.DeviceManager
|
import hep.dataforge.control.controllers.DeviceManager
|
||||||
import hep.dataforge.control.controllers.installing
|
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.SimpleIntegerProperty
|
||||||
|
import javafx.beans.property.SimpleObjectProperty
|
||||||
import javafx.beans.property.SimpleStringProperty
|
import javafx.beans.property.SimpleStringProperty
|
||||||
|
import javafx.collections.FXCollections
|
||||||
import javafx.scene.Parent
|
import javafx.scene.Parent
|
||||||
|
import javafx.scene.layout.Priority
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import tornadofx.*
|
import tornadofx.*
|
||||||
|
|
||||||
class PiMotionMasterApp : App(PiMotionMasterView::class)
|
class PiMotionMasterApp : App(PiMotionMasterView::class)
|
||||||
@ -25,33 +30,98 @@ class PiMotionMasterController : Controller() {
|
|||||||
class PiMotionMasterView : View() {
|
class PiMotionMasterView : View() {
|
||||||
|
|
||||||
private val controller: PiMotionMasterController by inject()
|
private val controller: PiMotionMasterController by inject()
|
||||||
|
val device = controller.motionMaster
|
||||||
|
|
||||||
|
private val connectedProperty: ReadOnlyProperty<Boolean> = device.connected.fxProperty(device)
|
||||||
|
private val debugServerJobProperty = SimpleObjectProperty<Job>()
|
||||||
|
private val debugServerStarted = debugServerJobProperty.booleanBinding { it != null }
|
||||||
|
private val axisList = FXCollections.observableArrayList<Map.Entry<String, PiMotionMasterDevice.Axis>>()
|
||||||
|
|
||||||
override val root: Parent = borderpane {
|
override val root: Parent = borderpane {
|
||||||
top {
|
top {
|
||||||
form {
|
form {
|
||||||
val host = SimpleStringProperty("127.0.0.1")
|
val host = SimpleStringProperty("127.0.0.1")
|
||||||
val port = SimpleIntegerProperty(10024)
|
val port = SimpleIntegerProperty(10024)
|
||||||
val virtual = SimpleBooleanProperty(false)
|
|
||||||
fieldset("Address:") {
|
fieldset("Address:") {
|
||||||
field("Host:") {
|
field("Host:") {
|
||||||
textfield(host){
|
textfield(host) {
|
||||||
enableWhen(virtual.not())
|
enableWhen(debugServerStarted.not())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
field("Port:") {
|
field("Port:") {
|
||||||
textfield(port)
|
textfield(port){
|
||||||
}
|
stripNonNumeric()
|
||||||
field("Virtual device:") {
|
}
|
||||||
checkbox(property = virtual)
|
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") {
|
button {
|
||||||
action {
|
hgrow = Priority.ALWAYS
|
||||||
if(virtual.get()){
|
textProperty().bind(connectedProperty.stringBinding {
|
||||||
controller.context.launchPiDebugServer(port.get(), listOf("1", "2"))
|
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())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -22,19 +22,68 @@ import kotlin.time.Duration
|
|||||||
|
|
||||||
class PiMotionMasterDevice(
|
class PiMotionMasterDevice(
|
||||||
context: Context,
|
context: Context,
|
||||||
private val portFactory: PortFactory = TcpPort,
|
private val portFactory: PortFactory = KtorTcpPort,
|
||||||
) : DeviceBase(context), DeviceHub {
|
) : DeviceBase(context), DeviceHub {
|
||||||
|
|
||||||
override val scope: CoroutineScope = CoroutineScope(
|
override val scope: CoroutineScope = CoroutineScope(
|
||||||
context.coroutineContext + SupervisorJob(context.coroutineContext[Job])
|
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 = {
|
val connected by readingBoolean(false, descriptorBuilder = {
|
||||||
info = "True if the connection address is defined and the device is initialized"
|
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()) {
|
val timeout: DeviceProperty by writingVirtual(200.asValue()) {
|
||||||
@ -43,8 +92,6 @@ class PiMotionMasterDevice(
|
|||||||
|
|
||||||
var timeoutValue: Duration by timeout.duration()
|
var timeoutValue: Duration by timeout.duration()
|
||||||
|
|
||||||
private val connector = PortProxy { portFactory(address ?: error("The device is not connected"), context) }
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Name-friendly accessor for axis
|
* Name-friendly accessor for axis
|
||||||
*/
|
*/
|
||||||
@ -58,46 +105,8 @@ class PiMotionMasterDevice(
|
|||||||
if (errorCode != 0) error(message(errorCode))
|
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) {
|
fun connect(host: String, port: Int) {
|
||||||
scope.launch {
|
runBlocking {
|
||||||
connect(Meta {
|
connect(Meta {
|
||||||
"host" put host
|
"host" put host
|
||||||
"port" put port
|
"port" put port
|
||||||
@ -119,14 +128,14 @@ class PiMotionMasterDevice(
|
|||||||
arguments.joinToString(prefix = " ", separator = " ", postfix = "")
|
arguments.joinToString(prefix = " ", separator = " ", postfix = "")
|
||||||
}
|
}
|
||||||
val stringToSend = "$command$joinedArguments\n"
|
val stringToSend = "$command$joinedArguments\n"
|
||||||
connector.send(stringToSend)
|
port?.send(stringToSend) ?: error("Not connected to device")
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getErrorCode(): Int = mutex.withLock {
|
suspend fun getErrorCode(): Int = mutex.withLock {
|
||||||
withTimeout(timeoutValue) {
|
withTimeout(timeoutValue) {
|
||||||
sendCommandInternal("ERR?")
|
sendCommandInternal("ERR?")
|
||||||
val errorString = connector.receiving().withDelimiter("\n").first()
|
val errorString = port?.receiving()?.withDelimiter("\n")?.first() ?: error("Not connected to device")
|
||||||
errorString.toInt()
|
errorString.trim().toInt()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -137,17 +146,12 @@ class PiMotionMasterDevice(
|
|||||||
try {
|
try {
|
||||||
withTimeout(timeoutValue) {
|
withTimeout(timeoutValue) {
|
||||||
sendCommandInternal(command, *arguments)
|
sendCommandInternal(command, *arguments)
|
||||||
val phrases = connector.receiving().withDelimiter("\n")
|
val phrases = port?.receiving()?.withDelimiter("\n") ?: error("Not connected to device")
|
||||||
var lastLineFlag = false
|
val list = phrases.transformWhile { line ->
|
||||||
phrases.transformWhile { line ->
|
emit(line)
|
||||||
if (lastLineFlag) {
|
line.endsWith(" \n")
|
||||||
false
|
|
||||||
} else {
|
|
||||||
emit(line)
|
|
||||||
lastLineFlag = !line.endsWith(" \n")
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}.toList()
|
}.toList()
|
||||||
|
list
|
||||||
}
|
}
|
||||||
} catch (ex: Throwable) {
|
} catch (ex: Throwable) {
|
||||||
logger.warn { "Error during PIMotionMaster request. Requesting error code." }
|
logger.warn { "Error during PIMotionMaster request. Requesting error code." }
|
||||||
@ -273,6 +277,26 @@ class PiMotionMasterDevice(
|
|||||||
send("FRF", axisId)
|
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<Double> by axisNumberProperty("POS") {
|
val position: TypedDeviceProperty<Double> by axisNumberProperty("POS") {
|
||||||
info = "The current axis position."
|
info = "The current axis position."
|
||||||
}
|
}
|
||||||
@ -303,6 +327,10 @@ class PiMotionMasterDevice(
|
|||||||
delay(200)
|
delay(200)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun move(target: Double) {
|
||||||
|
move(target.asMetaItem())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object : DeviceFactory<PiMotionMasterDevice> {
|
companion object : DeviceFactory<PiMotionMasterDevice> {
|
||||||
|
@ -148,6 +148,14 @@ class PiMotionMasterVirtualDevice(
|
|||||||
respond(response)
|
respond(response)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun doForEachAxis(parts: List<String>, 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) {
|
override suspend fun evaluateRequest(request: ByteArray) {
|
||||||
assert(request.last() == '\n'.toByte())
|
assert(request.last() == '\n'.toByte())
|
||||||
val string = request.decodeToString().substringBefore("\n")
|
val string = request.decodeToString().substringBefore("\n")
|
||||||
@ -222,7 +230,7 @@ class PiMotionMasterVirtualDevice(
|
|||||||
respond(errorCode.toString())
|
respond(errorCode.toString())
|
||||||
errorCode = 0
|
errorCode = 0
|
||||||
}
|
}
|
||||||
"SAI?" -> respond(axisIds.joinToString(separator = " \n"))
|
"SAI?" -> respond(axisState.keys.joinToString(separator = " \n"))
|
||||||
"CST?" -> respondForAllAxis(axisIds) { "L-220.20SG" }
|
"CST?" -> respondForAllAxis(axisIds) { "L-220.20SG" }
|
||||||
"RON?" -> respondForAllAxis(axisIds) { referenceMode }
|
"RON?" -> respondForAllAxis(axisIds) { referenceMode }
|
||||||
"FRF?" -> respondForAllAxis(axisIds) { "1" } // WAT?
|
"FRF?" -> respondForAllAxis(axisIds) { "1" } // WAT?
|
||||||
@ -233,10 +241,17 @@ class PiMotionMasterVirtualDevice(
|
|||||||
"TMX?" -> respondForAllAxis(axisIds) { maxPosition }
|
"TMX?" -> respondForAllAxis(axisIds) { maxPosition }
|
||||||
"VEL?" -> respondForAllAxis(axisIds) { velocity }
|
"VEL?" -> respondForAllAxis(axisIds) { velocity }
|
||||||
"SRG?" -> respond(WAT)
|
"SRG?" -> respond(WAT)
|
||||||
"SVO" -> {
|
"SVO" -> doForEachAxis(parts) { key, value ->
|
||||||
val requestAxis = parts[1]
|
axisState[key]?.servoMode = value.toInt()
|
||||||
val servoMode = parts.last()
|
}
|
||||||
axisState[requestAxis]?.servoMode = servoMode.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 -> {
|
else -> {
|
||||||
logger.warn { "Unknown command: $command in message ${String(request)}" }
|
logger.warn { "Unknown command: $command in message ${String(request)}" }
|
||||||
|
@ -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 <T : Any> TypedReadOnlyDeviceProperty<T>.fxProperty(ownerDevice: Device?): ReadOnlyProperty<T> =
|
||||||
|
object : ObjectPropertyBase<T>() {
|
||||||
|
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 <T : Any> TypedDeviceProperty<T>.fxProperty(ownerDevice: Device?): Property<T> =
|
||||||
|
object : ObjectPropertyBase<T>() {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -3,7 +3,7 @@ pluginManagement {
|
|||||||
val toolsVersion = "0.6.3-dev-1.4.20-M1"
|
val toolsVersion = "0.6.3-dev-1.4.20-M1"
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
mavenLocal()
|
//mavenLocal()
|
||||||
jcenter()
|
jcenter()
|
||||||
gradlePluginPortal()
|
gradlePluginPortal()
|
||||||
maven("https://kotlin.bintray.com/kotlinx")
|
maven("https://kotlin.bintray.com/kotlinx")
|
||||||
@ -27,8 +27,8 @@ pluginManagement {
|
|||||||
rootProject.name = "dataforge-control"
|
rootProject.name = "dataforge-control"
|
||||||
|
|
||||||
include(
|
include(
|
||||||
":ktor-sse",
|
|
||||||
":dataforge-device-core",
|
":dataforge-device-core",
|
||||||
|
":dataforge-device-tcp",
|
||||||
":dataforge-device-serial",
|
":dataforge-device-serial",
|
||||||
":dataforge-device-server",
|
":dataforge-device-server",
|
||||||
":dataforge-magix-client",
|
":dataforge-magix-client",
|
||||||
|
Loading…
Reference in New Issue
Block a user