[WIP] moving from java fx to compose in examples
This commit is contained in:
parent
24b6856f15
commit
44514cd477
@ -48,7 +48,7 @@ public operator fun DeviceHub.get(nameToken: NameToken): Device =
|
||||
|
||||
public fun DeviceHub.getOrNull(name: Name): Device? = when {
|
||||
name.isEmpty() -> this as? Device
|
||||
name.length == 1 -> get(name.firstOrNull()!!)
|
||||
name.length == 1 -> devices[name.firstOrNull()!!]
|
||||
else -> (get(name.firstOrNull()!!) as? DeviceHub)?.getOrNull(name.cutFirst())
|
||||
}
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
package space.kscience.controls.demo
|
||||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
@ -173,7 +173,7 @@ fun main() = application {
|
||||
controller.shutdown()
|
||||
exitApplication()
|
||||
},
|
||||
state = rememberWindowState(width = 400.dp, height = 300.dp)
|
||||
state = rememberWindowState(width = 400.dp, height = 320.dp)
|
||||
) {
|
||||
MaterialTheme {
|
||||
DemoControls(controller)
|
||||
|
@ -3,10 +3,6 @@ plugins {
|
||||
alias(spclibs.plugins.compose)
|
||||
}
|
||||
|
||||
//application{
|
||||
// mainClass.set("ru.mipt.npm.devices.pimotionmaster.PiMotionMasterAppKt")
|
||||
//}
|
||||
|
||||
kotlin{
|
||||
explicitApi = null
|
||||
}
|
||||
@ -17,4 +13,17 @@ val dataforgeVersion: String by extra
|
||||
dependencies {
|
||||
implementation(project(":controls-ports-ktor"))
|
||||
implementation(projects.controlsMagix)
|
||||
|
||||
implementation(compose.runtime)
|
||||
implementation(compose.desktop.currentOs)
|
||||
implementation(compose.material3)
|
||||
implementation(spclibs.logback.classic)
|
||||
}
|
||||
|
||||
compose{
|
||||
desktop{
|
||||
application{
|
||||
mainClass = "ru.mipt.npm.devices.pimotionmaster.PiMotionMasterAppKt"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,31 +1,193 @@
|
||||
package ru.mipt.npm.devices.pimotionmaster
|
||||
|
||||
import javafx.beans.property.ReadOnlyProperty
|
||||
import javafx.beans.property.SimpleIntegerProperty
|
||||
import javafx.beans.property.SimpleObjectProperty
|
||||
import javafx.beans.property.SimpleStringProperty
|
||||
import javafx.geometry.Pos
|
||||
import javafx.scene.Parent
|
||||
import javafx.scene.layout.Priority
|
||||
import javafx.scene.layout.VBox
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.material.Button
|
||||
import androidx.compose.material.OutlinedTextField
|
||||
import androidx.compose.material.Slider
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.Window
|
||||
import androidx.compose.ui.window.application
|
||||
import androidx.compose.ui.window.rememberWindowState
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import ru.mipt.npm.devices.pimotionmaster.PiMotionMasterDevice.Axis.Companion.maxPosition
|
||||
import ru.mipt.npm.devices.pimotionmaster.PiMotionMasterDevice.Axis.Companion.minPosition
|
||||
import ru.mipt.npm.devices.pimotionmaster.PiMotionMasterDevice.Axis.Companion.position
|
||||
import space.kscience.controls.manager.DeviceManager
|
||||
import space.kscience.controls.manager.installing
|
||||
import space.kscience.controls.spec.read
|
||||
import space.kscience.dataforge.context.Context
|
||||
import space.kscience.dataforge.context.request
|
||||
import tornadofx.*
|
||||
|
||||
class PiMotionMasterApp : App(PiMotionMasterView::class)
|
||||
//class PiMotionMasterApp : App(PiMotionMasterView::class)
|
||||
//
|
||||
//class PiMotionMasterController : Controller() {
|
||||
// //initialize context
|
||||
// val context = Context("piMotionMaster") {
|
||||
// plugin(DeviceManager)
|
||||
// }
|
||||
//
|
||||
// //initialize deviceManager plugin
|
||||
// val deviceManager: DeviceManager = context.request(DeviceManager)
|
||||
//
|
||||
// // install device
|
||||
// val motionMaster: PiMotionMasterDevice by deviceManager.installing(PiMotionMasterDevice)
|
||||
//}
|
||||
|
||||
class PiMotionMasterController : Controller() {
|
||||
//initialize context
|
||||
val context = Context("piMotionMaster"){
|
||||
@Composable
|
||||
fun ColumnScope.piMotionMasterAxis(
|
||||
axisName: String,
|
||||
axis: PiMotionMasterDevice.Axis,
|
||||
) {
|
||||
Row {
|
||||
Text(axisName)
|
||||
var min by remember { mutableStateOf(0f) }
|
||||
var max by remember { mutableStateOf(0f) }
|
||||
var targetPosition by remember { mutableStateOf(0f) }
|
||||
val position: Double by axis.composeState(PiMotionMasterDevice.Axis.position, 0.0)
|
||||
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
LaunchedEffect(axis) {
|
||||
min = axis.read(PiMotionMasterDevice.Axis.minPosition).toFloat()
|
||||
max = axis.read(PiMotionMasterDevice.Axis.maxPosition).toFloat()
|
||||
targetPosition = axis.read(PiMotionMasterDevice.Axis.position).toFloat()
|
||||
}
|
||||
|
||||
Column {
|
||||
Slider(
|
||||
value = position.toFloat(),
|
||||
enabled = false,
|
||||
onValueChange = { },
|
||||
valueRange = min..max
|
||||
)
|
||||
Slider(
|
||||
value = targetPosition,
|
||||
onValueChange = { newPosition ->
|
||||
scope.launch {
|
||||
axis.move(newPosition.toDouble())
|
||||
}
|
||||
targetPosition = newPosition
|
||||
},
|
||||
valueRange = min..max
|
||||
)
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun AxisPane(axes: Map<String, PiMotionMasterDevice.Axis>) {
|
||||
Column {
|
||||
axes.forEach { (name, axis) ->
|
||||
this.piMotionMasterAxis(name, axis)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Composable
|
||||
fun PiMotionMasterApp(device: PiMotionMasterDevice) {
|
||||
|
||||
val scope = rememberCoroutineScope()
|
||||
val connected by device.composeState(PiMotionMasterDevice.connected, false)
|
||||
var debugServerJob by remember { mutableStateOf<Job?>(null) }
|
||||
var axes by remember { mutableStateOf<Map<String, PiMotionMasterDevice.Axis>?>(null) }
|
||||
//private val axisList = FXCollections.observableArrayList<Map.Entry<String, PiMotionMasterDevice.Axis>>()
|
||||
var host by remember { mutableStateOf("127.0.0.1") }
|
||||
var port by remember { mutableStateOf(10024) }
|
||||
|
||||
Scaffold {
|
||||
Column {
|
||||
|
||||
|
||||
Text("Address:")
|
||||
Row {
|
||||
OutlinedTextField(
|
||||
value = host,
|
||||
onValueChange = { host = it },
|
||||
label = { Text("Host") },
|
||||
enabled = debugServerJob == null,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
var portError by remember { mutableStateOf(false) }
|
||||
OutlinedTextField(
|
||||
value = port.toString(),
|
||||
onValueChange = {
|
||||
it.toIntOrNull()?.let { value ->
|
||||
port = value
|
||||
portError = false
|
||||
} ?: run {
|
||||
portError = true
|
||||
}
|
||||
},
|
||||
label = { Text("Port") },
|
||||
enabled = debugServerJob == null,
|
||||
isError = portError,
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
}
|
||||
Row {
|
||||
Button(
|
||||
onClick = {
|
||||
if (debugServerJob == null) {
|
||||
debugServerJob = device.context.launchPiDebugServer(port, listOf("1", "2", "3", "4"))
|
||||
} else {
|
||||
debugServerJob?.cancel()
|
||||
debugServerJob = null
|
||||
}
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
if (debugServerJob == null) {
|
||||
Text("Start debug server")
|
||||
} else {
|
||||
Text("Stop debug server")
|
||||
}
|
||||
}
|
||||
}
|
||||
Row {
|
||||
Button(
|
||||
onClick = {
|
||||
if (!connected) {
|
||||
device.launch {
|
||||
device.connect(host, port)
|
||||
}
|
||||
axes = device.axes
|
||||
} else {
|
||||
device.launch {
|
||||
device.disconnect()
|
||||
}
|
||||
axes = null
|
||||
}
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
if (!connected) {
|
||||
Text("Connect")
|
||||
} else {
|
||||
Text("Disconnect")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
axes?.let { axes ->
|
||||
AxisPane(axes)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun main() = application {
|
||||
|
||||
val context = Context("piMotionMaster") {
|
||||
plugin(DeviceManager)
|
||||
}
|
||||
|
||||
@ -34,131 +196,14 @@ class PiMotionMasterController : Controller() {
|
||||
|
||||
// install device
|
||||
val motionMaster: PiMotionMasterDevice by deviceManager.installing(PiMotionMasterDevice)
|
||||
}
|
||||
|
||||
fun VBox.piMotionMasterAxis(
|
||||
axisName: String,
|
||||
axis: PiMotionMasterDevice.Axis,
|
||||
coroutineScope: CoroutineScope,
|
||||
) = hbox {
|
||||
alignment = Pos.CENTER
|
||||
label(axisName)
|
||||
coroutineScope.launch {
|
||||
with(axis) {
|
||||
val min: Double = read(minPosition)
|
||||
val max: Double = read(maxPosition)
|
||||
val positionProperty = fxProperty(position)
|
||||
val startPosition = read(position)
|
||||
runLater {
|
||||
vbox {
|
||||
hgrow = Priority.ALWAYS
|
||||
slider(min..max, startPosition) {
|
||||
minWidth = 300.0
|
||||
isShowTickLabels = true
|
||||
isShowTickMarks = true
|
||||
minorTickCount = 10
|
||||
majorTickUnit = 1.0
|
||||
valueProperty().onChange {
|
||||
coroutineScope.launch {
|
||||
axis.move(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
slider(min..max) {
|
||||
isDisable = true
|
||||
valueProperty().bind(positionProperty)
|
||||
}
|
||||
}
|
||||
}
|
||||
Window(
|
||||
title = "Pi motion master demo",
|
||||
onCloseRequest = { exitApplication() },
|
||||
state = rememberWindowState(width = 400.dp, height = 300.dp)
|
||||
) {
|
||||
MaterialTheme {
|
||||
PiMotionMasterApp(motionMaster)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun Parent.axisPane(axes: Map<String, PiMotionMasterDevice.Axis>, coroutineScope: CoroutineScope) {
|
||||
vbox {
|
||||
axes.forEach { (name, axis) ->
|
||||
this.piMotionMasterAxis(name, axis, coroutineScope)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class PiMotionMasterView : View() {
|
||||
|
||||
private val controller: PiMotionMasterController by inject()
|
||||
val device = controller.motionMaster
|
||||
|
||||
private val connectedProperty: ReadOnlyProperty<Boolean> = device.fxProperty(PiMotionMasterDevice.connected)
|
||||
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 {
|
||||
top {
|
||||
form {
|
||||
val host = SimpleStringProperty("127.0.0.1")
|
||||
val port = SimpleIntegerProperty(10024)
|
||||
fieldset("Address:") {
|
||||
field("Host:") {
|
||||
textfield(host) {
|
||||
enableWhen(debugServerStarted.not())
|
||||
}
|
||||
}
|
||||
field("Port:") {
|
||||
textfield(port) {
|
||||
stripNonNumeric()
|
||||
}
|
||||
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", "3", "4"))
|
||||
} else {
|
||||
debugServerJobProperty.get().cancel()
|
||||
debugServerJobProperty.value = null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
button {
|
||||
hgrow = Priority.ALWAYS
|
||||
textProperty().bind(connectedProperty.stringBinding {
|
||||
if (it == false) {
|
||||
"Connect"
|
||||
} else {
|
||||
"Disconnect"
|
||||
}
|
||||
})
|
||||
action {
|
||||
if (!connectedProperty.value) {
|
||||
device.connect(host.get(), port.get())
|
||||
center {
|
||||
axisPane(device.axes,controller.context)
|
||||
}
|
||||
} else {
|
||||
this@borderpane.center = null
|
||||
device.disconnect()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
fun main() {
|
||||
launch<PiMotionMasterApp>()
|
||||
}
|
@ -7,13 +7,15 @@ import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.toList
|
||||
import kotlinx.coroutines.flow.transformWhile
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.withTimeout
|
||||
import space.kscience.controls.api.DeviceHub
|
||||
import space.kscience.controls.api.PropertyDescriptor
|
||||
import space.kscience.controls.ports.*
|
||||
import space.kscience.controls.ports.AsynchronousPort
|
||||
import space.kscience.controls.ports.KtorTcpPort
|
||||
import space.kscience.controls.ports.send
|
||||
import space.kscience.controls.ports.withStringDelimiter
|
||||
import space.kscience.controls.spec.*
|
||||
import space.kscience.dataforge.context.*
|
||||
import space.kscience.dataforge.meta.*
|
||||
@ -33,10 +35,8 @@ class PiMotionMasterDevice(
|
||||
//PortProxy { portFactory(address ?: error("The device is not connected"), context) }
|
||||
|
||||
|
||||
fun disconnect() {
|
||||
runBlocking {
|
||||
execute(disconnect)
|
||||
}
|
||||
suspend fun disconnect() {
|
||||
execute(disconnect)
|
||||
}
|
||||
|
||||
var timeoutValue: Duration = 200.milliseconds
|
||||
@ -54,13 +54,11 @@ class PiMotionMasterDevice(
|
||||
if (errorCode != 0) error(message(errorCode))
|
||||
}
|
||||
|
||||
fun connect(host: String, port: Int) {
|
||||
runBlocking {
|
||||
execute(connect, Meta {
|
||||
"host" put host
|
||||
"port" put port
|
||||
})
|
||||
}
|
||||
suspend fun connect(host: String, port: Int) {
|
||||
execute(connect, Meta {
|
||||
"host" put host
|
||||
"port" put port
|
||||
})
|
||||
}
|
||||
|
||||
private val mutex = Mutex()
|
||||
@ -103,7 +101,7 @@ class PiMotionMasterDevice(
|
||||
}.toList()
|
||||
}
|
||||
} catch (ex: Throwable) {
|
||||
logger.warn { "Error during PIMotionMaster request. Requesting error code." }
|
||||
logger.error(ex) { "Error during PIMotionMaster request. Requesting error code." }
|
||||
val errorCode = getErrorCode()
|
||||
dispatchError(errorCode)
|
||||
logger.warn { "Error code $errorCode" }
|
||||
|
@ -0,0 +1,15 @@
|
||||
package ru.mipt.npm.devices.pimotionmaster
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import space.kscience.controls.api.Device
|
||||
import space.kscience.controls.spec.DevicePropertySpec
|
||||
import space.kscience.controls.spec.propertyFlow
|
||||
|
||||
|
||||
@Composable
|
||||
fun <D : Device, T : Any> D.composeState(
|
||||
spec: DevicePropertySpec<D, T>,
|
||||
initialState: T,
|
||||
): State<T> = propertyFlow(spec).collectAsState(initialState)
|
@ -1,58 +0,0 @@
|
||||
package ru.mipt.npm.devices.pimotionmaster
|
||||
|
||||
import javafx.beans.property.ObjectPropertyBase
|
||||
import javafx.beans.property.Property
|
||||
import javafx.beans.property.ReadOnlyProperty
|
||||
import space.kscience.controls.api.Device
|
||||
import space.kscience.controls.spec.*
|
||||
import space.kscience.dataforge.context.info
|
||||
import space.kscience.dataforge.context.logger
|
||||
import tornadofx.*
|
||||
|
||||
/**
|
||||
* Bind a FX property to a device property with a given [spec]
|
||||
*/
|
||||
fun <D : Device, T : Any> D.fxProperty(
|
||||
spec: DevicePropertySpec<D, T>,
|
||||
): ReadOnlyProperty<T> = object : ObjectPropertyBase<T>() {
|
||||
override fun getBean(): Any = this
|
||||
override fun getName(): String = spec.name
|
||||
|
||||
init {
|
||||
//Read incoming changes
|
||||
onPropertyChange(spec) {
|
||||
runLater {
|
||||
try {
|
||||
set(it)
|
||||
} catch (ex: Throwable) {
|
||||
logger.info { "Failed to set property $name to $it" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun <D : Device, T : Any> D.fxProperty(spec: MutableDevicePropertySpec<D, T>): Property<T> =
|
||||
object : ObjectPropertyBase<T>() {
|
||||
override fun getBean(): Any = this
|
||||
override fun getName(): String = spec.name
|
||||
|
||||
init {
|
||||
//Read incoming changes
|
||||
onPropertyChange(spec) {
|
||||
runLater {
|
||||
try {
|
||||
set(it)
|
||||
} catch (ex: Throwable) {
|
||||
logger.info { "Failed to set property $name to $it" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onChange { newValue ->
|
||||
if (newValue != null) {
|
||||
writeAsync(spec, newValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -18,41 +18,41 @@ val exceptionHandler = CoroutineExceptionHandler { _, throwable ->
|
||||
@OptIn(InternalAPI::class)
|
||||
fun Context.launchPiDebugServer(port: Int, axes: List<String>): Job = launch(exceptionHandler) {
|
||||
val virtualDevice = PiMotionMasterVirtualDevice(this@launchPiDebugServer, axes)
|
||||
val server = aSocket(ActorSelectorManager(Dispatchers.IO)).tcp().bind("localhost", port)
|
||||
println("Started virtual port server at ${server.localAddress}")
|
||||
aSocket(ActorSelectorManager(Dispatchers.IO)).tcp().bind("localhost", port).use { server ->
|
||||
println("Started virtual port server at ${server.localAddress}")
|
||||
|
||||
while (isActive) {
|
||||
val socket = server.accept()
|
||||
launch(SupervisorJob(coroutineContext[Job])) {
|
||||
println("Socket accepted: ${socket.remoteAddress}")
|
||||
val input = socket.openReadChannel()
|
||||
val output = socket.openWriteChannel()
|
||||
while (isActive) {
|
||||
val socket = server.accept()
|
||||
launch(SupervisorJob(coroutineContext[Job])) {
|
||||
println("Socket accepted: ${socket.remoteAddress}")
|
||||
val input = socket.openReadChannel()
|
||||
val output = socket.openWriteChannel()
|
||||
|
||||
val sendJob = launch {
|
||||
virtualDevice.subscribe().collect {
|
||||
//println("Sending: ${it.decodeToString()}")
|
||||
output.writeAvailable(it)
|
||||
output.flush()
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
while (isActive) {
|
||||
input.read { buffer ->
|
||||
val array = buffer.moveToByteArray()
|
||||
launch {
|
||||
virtualDevice.send(array)
|
||||
}
|
||||
val sendJob = launch {
|
||||
virtualDevice.subscribe().collect {
|
||||
//println("Sending: ${it.decodeToString()}")
|
||||
output.writeAvailable(it)
|
||||
output.flush()
|
||||
}
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
e.printStackTrace()
|
||||
sendJob.cancel()
|
||||
socket.close()
|
||||
} finally {
|
||||
println("Socket closed")
|
||||
}
|
||||
|
||||
try {
|
||||
while (isActive) {
|
||||
input.read { buffer ->
|
||||
val array = buffer.moveToByteArray()
|
||||
launch {
|
||||
virtualDevice.send(array)
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
e.printStackTrace()
|
||||
sendJob.cancel()
|
||||
socket.close()
|
||||
} finally {
|
||||
println("Client socket closed")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user