[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 {
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())
}

View File

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

View File

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

View File

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

View File

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

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,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")
}
}
}
}
}