[WIP] moving from java fx to compose in examples

This commit is contained in:
Alexander Nozik 2024-05-12 13:52:00 +03:00
parent 24b6856f15
commit 44514cd477
8 changed files with 259 additions and 250 deletions

View File

@ -48,7 +48,7 @@ public operator fun DeviceHub.get(nameToken: NameToken): Device =
public fun DeviceHub.getOrNull(name: Name): Device? = when { public fun DeviceHub.getOrNull(name: Name): Device? = when {
name.isEmpty() -> this as? Device 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()) else -> (get(name.firstOrNull()!!) as? DeviceHub)?.getOrNull(name.cutFirst())
} }

View File

@ -1,7 +1,7 @@
package space.kscience.controls.demo package space.kscience.controls.demo
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.material.* import androidx.compose.material3.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@ -173,7 +173,7 @@ fun main() = application {
controller.shutdown() controller.shutdown()
exitApplication() exitApplication()
}, },
state = rememberWindowState(width = 400.dp, height = 300.dp) state = rememberWindowState(width = 400.dp, height = 320.dp)
) { ) {
MaterialTheme { MaterialTheme {
DemoControls(controller) DemoControls(controller)

View File

@ -3,10 +3,6 @@ plugins {
alias(spclibs.plugins.compose) alias(spclibs.plugins.compose)
} }
//application{
// mainClass.set("ru.mipt.npm.devices.pimotionmaster.PiMotionMasterAppKt")
//}
kotlin{ kotlin{
explicitApi = null explicitApi = null
} }
@ -17,4 +13,17 @@ val dataforgeVersion: String by extra
dependencies { dependencies {
implementation(project(":controls-ports-ktor")) implementation(project(":controls-ports-ktor"))
implementation(projects.controlsMagix) 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"
}
}
} }

View File

@ -1,31 +1,193 @@
package ru.mipt.npm.devices.pimotionmaster package ru.mipt.npm.devices.pimotionmaster
import javafx.beans.property.ReadOnlyProperty
import javafx.beans.property.SimpleIntegerProperty import androidx.compose.foundation.layout.Column
import javafx.beans.property.SimpleObjectProperty import androidx.compose.foundation.layout.ColumnScope
import javafx.beans.property.SimpleStringProperty import androidx.compose.foundation.layout.Row
import javafx.geometry.Pos import androidx.compose.foundation.layout.fillMaxWidth
import javafx.scene.Parent import androidx.compose.material.Button
import javafx.scene.layout.Priority import androidx.compose.material.OutlinedTextField
import javafx.scene.layout.VBox import androidx.compose.material.Slider
import kotlinx.coroutines.CoroutineScope 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.Job
import kotlinx.coroutines.launch 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.DeviceManager
import space.kscience.controls.manager.installing import space.kscience.controls.manager.installing
import space.kscience.controls.spec.read import space.kscience.controls.spec.read
import space.kscience.dataforge.context.Context import space.kscience.dataforge.context.Context
import space.kscience.dataforge.context.request 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() { @Composable
//initialize context fun ColumnScope.piMotionMasterAxis(
val context = Context("piMotionMaster"){ 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) plugin(DeviceManager)
} }
@ -34,131 +196,14 @@ class PiMotionMasterController : Controller() {
// install device // install device
val motionMaster: PiMotionMasterDevice by deviceManager.installing(PiMotionMasterDevice) val motionMaster: PiMotionMasterDevice by deviceManager.installing(PiMotionMasterDevice)
}
fun VBox.piMotionMasterAxis( Window(
axisName: String, title = "Pi motion master demo",
axis: PiMotionMasterDevice.Axis, onCloseRequest = { exitApplication() },
coroutineScope: CoroutineScope, state = rememberWindowState(width = 400.dp, height = 300.dp)
) = hbox { ) {
alignment = Pos.CENTER MaterialTheme {
label(axisName) PiMotionMasterApp(motionMaster)
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)
}
}
}
} }
} }
} }
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>()
}

View File

@ -7,13 +7,15 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.toList import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.flow.transformWhile import kotlinx.coroutines.flow.transformWhile
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withTimeout import kotlinx.coroutines.withTimeout
import space.kscience.controls.api.DeviceHub import space.kscience.controls.api.DeviceHub
import space.kscience.controls.api.PropertyDescriptor 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.controls.spec.*
import space.kscience.dataforge.context.* import space.kscience.dataforge.context.*
import space.kscience.dataforge.meta.* import space.kscience.dataforge.meta.*
@ -33,11 +35,9 @@ class PiMotionMasterDevice(
//PortProxy { portFactory(address ?: error("The device is not connected"), context) } //PortProxy { portFactory(address ?: error("The device is not connected"), context) }
fun disconnect() { suspend fun disconnect() {
runBlocking {
execute(disconnect) execute(disconnect)
} }
}
var timeoutValue: Duration = 200.milliseconds var timeoutValue: Duration = 200.milliseconds
@ -54,14 +54,12 @@ class PiMotionMasterDevice(
if (errorCode != 0) error(message(errorCode)) if (errorCode != 0) error(message(errorCode))
} }
fun connect(host: String, port: Int) { suspend fun connect(host: String, port: Int) {
runBlocking {
execute(connect, Meta { execute(connect, Meta {
"host" put host "host" put host
"port" put port "port" put port
}) })
} }
}
private val mutex = Mutex() private val mutex = Mutex()
@ -103,7 +101,7 @@ class PiMotionMasterDevice(
}.toList() }.toList()
} }
} catch (ex: Throwable) { } 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() val errorCode = getErrorCode()
dispatchError(errorCode) dispatchError(errorCode)
logger.warn { "Error code $errorCode" } logger.warn { "Error code $errorCode" }

View File

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

View File

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

View File

@ -18,7 +18,7 @@ val exceptionHandler = CoroutineExceptionHandler { _, throwable ->
@OptIn(InternalAPI::class) @OptIn(InternalAPI::class)
fun Context.launchPiDebugServer(port: Int, axes: List<String>): Job = launch(exceptionHandler) { fun Context.launchPiDebugServer(port: Int, axes: List<String>): Job = launch(exceptionHandler) {
val virtualDevice = PiMotionMasterVirtualDevice(this@launchPiDebugServer, axes) val virtualDevice = PiMotionMasterVirtualDevice(this@launchPiDebugServer, axes)
val server = aSocket(ActorSelectorManager(Dispatchers.IO)).tcp().bind("localhost", port) aSocket(ActorSelectorManager(Dispatchers.IO)).tcp().bind("localhost", port).use { server ->
println("Started virtual port server at ${server.localAddress}") println("Started virtual port server at ${server.localAddress}")
while (isActive) { while (isActive) {
@ -50,9 +50,9 @@ fun Context.launchPiDebugServer(port: Int, axes: List<String>): Job = launch(exc
sendJob.cancel() sendJob.cancel()
socket.close() socket.close()
} finally { } finally {
println("Socket closed") println("Client socket closed")
}
} }
} }
} }
} }