Compare commits

..

No commits in common. "master" and "feature/device-collective-demo" have entirely different histories.

97 changed files with 390 additions and 1248 deletions
.space.ktsCHANGELOG.mdREADME.mdbuild.gradle.kts
controls-constructor
README.mdbuild.gradle.kts
src
commonMain/kotlin/space/kscience/controls/constructor
commonTest/kotlin/space/kscience/controls/constructor
controls-core
controls-jupyter
controls-magix
README.mdbuild.gradle.kts
src/commonMain/kotlin/space/kscience/controls/client
controls-modbus
controls-opcua
controls-pi
README.md
src/jvmMain/kotlin/space/kscience/controls/pi
controls-plc4x
controls-ports-ktor
README.md
src/jvmMain/kotlin/space/kscience/controls/ports
controls-serial
controls-server
controls-storage
README.md
controls-xodus
controls-vision
controls-visualisation-compose
demo
all-things/src/main/kotlin/space/kscience/controls/demo
car
constructor/src/jvmMain/kotlin
device-collective
echo
magix-demo/src/main/kotlin
many-devices
mks-pdr900/src/main/kotlin/center/sciprog/devices/mks
motors/src/main/kotlin/ru/mipt/npm/devices/pimotionmaster
gradle
magix
magix-api
magix-java-endpoint
magix-mqtt
magix-rabbit
README.md
src/main/kotlin/space/kscience/magix/rabbit
magix-rsocket
magix-server
magix-storage
magix-utils
magix-zmq
settings.gradle.kts
simulation-kt

45
.space.kts Normal file

@ -0,0 +1,45 @@
import kotlin.io.path.readText
job("Build") {
gradlew("spc.registry.jetbrains.space/p/sci/containers/kotlin-ci:1.0.3", "build")
}
job("Publish") {
startOn {
gitPush { enabled = false }
}
container("spc.registry.jetbrains.space/p/sci/containers/kotlin-ci:1.0.3") {
env["SPACE_USER"] = "{{ project:space_user }}"
env["SPACE_TOKEN"] = "{{ project:space_token }}"
kotlinScript { api ->
val spaceUser = System.getenv("SPACE_USER")
val spaceToken = System.getenv("SPACE_TOKEN")
// write the version to the build directory
api.gradlew("version")
//read the version from build file
val version = java.nio.file.Path.of("build/project-version.txt").readText()
val revisionSuffix = if (version.endsWith("SNAPSHOT")) {
"-" + api.gitRevision().take(7)
} else {
""
}
api.space().projects.automation.deployments.start(
project = api.projectIdentifier(),
targetIdentifier = TargetIdentifier.Key("maps-kt"),
version = version+revisionSuffix,
// automatically update deployment status based on the status of a job
syncWithAutomationJob = true
)
api.gradlew(
"publishAllPublicationsToSpaceRepository",
"-Ppublishing.space.user=\"$spaceUser\"",
"-Ppublishing.space.token=\"$spaceToken\"",
)
}
}
}

@ -8,8 +8,6 @@
- Shortcuts to access all Controls devices in a magix network. - Shortcuts to access all Controls devices in a magix network.
- `DeviceClient` properly evaluates lifecycle and logs - `DeviceClient` properly evaluates lifecycle and logs
- `PeerConnection` API for direct device-device binary sharing - `PeerConnection` API for direct device-device binary sharing
- DeviceDrawable2D intermediate visualization implementation
- New interface `WithLifeCycle`. Change Port API to adhere to it.
### Changed ### Changed
- Constructor properties return `DeviceState` in order to be able to subscribe to them - Constructor properties return `DeviceState` in order to be able to subscribe to them
@ -17,8 +15,6 @@
- `DeviceClient` now initializes property and action descriptors eagerly. - `DeviceClient` now initializes property and action descriptors eagerly.
- `DeviceHub` now works with `Name` instead of `NameToken`. Tree-like structure is made using `Path`. Device messages no longer have access to sub-devices. - `DeviceHub` now works with `Name` instead of `NameToken`. Tree-like structure is made using `Path`. Device messages no longer have access to sub-devices.
- Add some utility methods to ports. Synchronous port response could be now consumed as `Source`. - Add some utility methods to ports. Synchronous port response could be now consumed as `Source`.
- `DeviceLifecycleState` is replaced by `LifecycleState`.
### Deprecated ### Deprecated

@ -147,15 +147,6 @@ Automatically checks consistency.
> >
> **Maturity**: EXPERIMENTAL > **Maturity**: EXPERIMENTAL
### [simulation-kt](simulation-kt)
> A framework for combination of asynchronous simulations.
>
> **Maturity**: PROTOTYPE
>
> **Features:**
> - [timeline](simulation-kt/#) : Timeline is an ordered discrete history containing TimeLineEvent
### [controls-storage/controls-xodus](controls-storage/controls-xodus) ### [controls-storage/controls-xodus](controls-storage/controls-xodus)
> An implementation of controls-storage on top of JetBrains Xodus. > An implementation of controls-storage on top of JetBrains Xodus.
> >

@ -3,12 +3,11 @@ import space.kscience.gradle.useSPCTeam
plugins { plugins {
id("space.kscience.gradle.project") id("space.kscience.gradle.project")
alias(libs.plugins.versions)
} }
allprojects { allprojects {
group = "space.kscience" group = "space.kscience"
version = "0.4.0-dev-7" version = "0.4.0-dev-4"
repositories{ repositories{
google() google()
} }

@ -6,7 +6,7 @@ A low-code constructor for composite devices simulation
## Artifact: ## Artifact:
The Maven coordinates of this project are `space.kscience:controls-constructor:0.4.0-dev-7`. The Maven coordinates of this project are `space.kscience:controls-constructor:0.4.0-dev-4`.
**Gradle Kotlin DSL:** **Gradle Kotlin DSL:**
```kotlin ```kotlin
@ -16,6 +16,6 @@ repositories {
} }
dependencies { dependencies {
implementation("space.kscience:controls-constructor:0.4.0-dev-7") implementation("space.kscience:controls-constructor:0.4.0-dev-4")
} }
``` ```

@ -10,8 +10,6 @@ description = """
kscience{ kscience{
jvm() jvm()
js() js()
native()
wasm()
useCoroutines() useCoroutines()
useSerialization() useSerialization()
commonMain { commonMain {

@ -3,7 +3,7 @@ package space.kscience.controls.constructor
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
import space.kscience.controls.api.* import space.kscience.controls.api.*
import space.kscience.controls.api.LifecycleState.* import space.kscience.controls.api.DeviceLifecycleState.*
import space.kscience.controls.manager.DeviceManager import space.kscience.controls.manager.DeviceManager
import space.kscience.controls.manager.install import space.kscience.controls.manager.install
import space.kscience.controls.spec.DevicePropertySpec import space.kscience.controls.spec.DevicePropertySpec
@ -165,11 +165,11 @@ public open class DeviceGroup(
return action(argument) return action(argument)
} }
final override var lifecycleState: LifecycleState = LifecycleState.STOPPED final override var lifecycleState: DeviceLifecycleState = DeviceLifecycleState.STOPPED
private set private set
private suspend fun setLifecycleState(lifecycleState: LifecycleState) { private suspend fun setLifecycleState(lifecycleState: DeviceLifecycleState) {
this.lifecycleState = lifecycleState this.lifecycleState = lifecycleState
sharedMessageFlow.emit( sharedMessageFlow.emit(
DeviceLifeCycleMessage(lifecycleState) DeviceLifeCycleMessage(lifecycleState)

@ -14,7 +14,7 @@ import space.kscience.dataforge.context.Context
/** /**
* A device that detects if a motor hits the end of its range * Virtual [LimitSwitch]
*/ */
public class LimitSwitch( public class LimitSwitch(
context: Context, context: Context,

@ -21,26 +21,23 @@ import kotlin.time.DurationUnit
*/ */
public class StepDrive( public class StepDrive(
context: Context, context: Context,
ticksPerSecond: Double, ticksPerSecond: MutableDeviceState<Double>,
position: MutableDeviceState<Long> = MutableDeviceState(0), target: MutableDeviceState<Long> = MutableDeviceState(0),
private val writeTicks: suspend (ticks: Long, speed: Double) -> Unit = { _, _ -> }, private val writeTicks: suspend (ticks: Long, speed: Double) -> Unit = { _, _ -> },
) : DeviceConstructor(context) { ) : DeviceConstructor(context) {
public val target: MutableDeviceState<Long> by property( public val target: MutableDeviceState<Long> by property(MetaConverter.long, target)
MetaConverter.long,
MutableDeviceState<Long>(position.value)
)
public val speed: MutableDeviceState<Double> by property( public val speed: MutableDeviceState<Double> by property(MetaConverter.double, ticksPerSecond)
MetaConverter.double,
MutableDeviceState<Double>(ticksPerSecond) private val positionState = stateOf(target.value)
)
public val position: DeviceState<Long> by property(MetaConverter.long, positionState)
public val position: DeviceState<Long> by property(MetaConverter.long, position)
//FIXME round to zero problem //FIXME round to zero problem
private val ticker = onTimer(reads = setOf(target, position), writes = setOf(position)) { prev, next -> private val ticker = onTimer(reads = setOf(target, position), writes = setOf(position)) { prev, next ->
val tickSpeed = speed.value val tickSpeed = ticksPerSecond.value
val timeDelta = (next - prev).toDouble(DurationUnit.SECONDS) val timeDelta = (next - prev).toDouble(DurationUnit.SECONDS)
val ticksDelta: Long = target.value - position.value val ticksDelta: Long = target.value - position.value
val steps: Long = when { val steps: Long = when {
@ -49,7 +46,7 @@ public class StepDrive(
else -> return@onTimer else -> return@onTimer
} }
writeTicks(steps, tickSpeed) writeTicks(steps, tickSpeed)
position.value += steps positionState.value += steps
} }
} }

@ -11,7 +11,7 @@ import kotlinx.coroutines.flow.emptyFlow
*/ */
private class VirtualDeviceState<T>( private class VirtualDeviceState<T>(
initialValue: T, initialValue: T,
private val callback: (T) -> Unit = {} private val callback: (T) -> Unit = {},
) : MutableDeviceState<T> { ) : MutableDeviceState<T> {
private val flow = MutableStateFlow(initialValue) private val flow = MutableStateFlow(initialValue)
override val valueFlow: Flow<T> get() = flow override val valueFlow: Flow<T> get() = flow
@ -34,7 +34,7 @@ private class VirtualDeviceState<T>(
*/ */
public fun <T> MutableDeviceState( public fun <T> MutableDeviceState(
initialValue: T, initialValue: T,
callback: (T) -> Unit = {} callback: (T) -> Unit = {},
): MutableDeviceState<T> = VirtualDeviceState(initialValue, callback) ): MutableDeviceState<T> = VirtualDeviceState(initialValue, callback)

@ -4,7 +4,7 @@ import kotlinx.coroutines.flow.first
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import space.kscience.controls.api.Device import space.kscience.controls.api.Device
import space.kscience.controls.api.DeviceLifeCycleMessage import space.kscience.controls.api.DeviceLifeCycleMessage
import space.kscience.controls.api.LifecycleState import space.kscience.controls.api.DeviceLifecycleState
import space.kscience.controls.manager.DeviceManager import space.kscience.controls.manager.DeviceManager
import space.kscience.controls.manager.install import space.kscience.controls.manager.install
import space.kscience.controls.spec.doRecurring import space.kscience.controls.spec.doRecurring
@ -37,7 +37,7 @@ class DeviceGroupTest {
} }
error("Error!") error("Error!")
} }
testDevice.messageFlow.first { it is DeviceLifeCycleMessage && it.state == LifecycleState.STOPPED } testDevice.messageFlow.first { it is DeviceLifeCycleMessage && it.state == DeviceLifecycleState.STOPPED }
println("stopped") println("stopped")
} }
} }

@ -16,7 +16,7 @@ Core interfaces for building a device server
## Artifact: ## Artifact:
The Maven coordinates of this project are `space.kscience:controls-core:0.4.0-dev-7`. The Maven coordinates of this project are `space.kscience:controls-core:0.4.0-dev-4`.
**Gradle Kotlin DSL:** **Gradle Kotlin DSL:**
```kotlin ```kotlin
@ -26,6 +26,6 @@ repositories {
} }
dependencies { dependencies {
implementation("space.kscience:controls-core:0.4.0-dev-7") implementation("space.kscience:controls-core:0.4.0-dev-4")
} }
``` ```

@ -13,7 +13,6 @@ kscience {
jvm() jvm()
js() js()
native() native()
wasm()
useCoroutines() useCoroutines()
useSerialization{ useSerialization{
json() json()

@ -5,7 +5,7 @@ import kotlinx.coroutines.flow.Flow
/** /**
* A generic bidirectional asynchronous sender/receiver object * A generic bidirectional asynchronous sender/receiver object
*/ */
public interface AsynchronousSocket<T> : WithLifeCycle { public interface AsynchronousSocket<T> : AutoCloseable {
/** /**
* Send an object to the socket * Send an object to the socket
*/ */
@ -15,6 +15,16 @@ public interface AsynchronousSocket<T> : WithLifeCycle {
* Flow of objects received from socket * Flow of objects received from socket
*/ */
public fun subscribe(): Flow<T> public fun subscribe(): Flow<T>
/**
* Start socket operation
*/
public fun open()
/**
* Check if this socket is open
*/
public val isOpen: Boolean
} }
/** /**

@ -4,6 +4,7 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.cancel import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
import kotlinx.serialization.Serializable
import space.kscience.controls.api.Device.Companion.DEVICE_TARGET import space.kscience.controls.api.Device.Companion.DEVICE_TARGET
import space.kscience.dataforge.context.ContextAware import space.kscience.dataforge.context.ContextAware
import space.kscience.dataforge.context.info import space.kscience.dataforge.context.info
@ -14,13 +15,40 @@ import space.kscience.dataforge.meta.string
import space.kscience.dataforge.misc.DfType import space.kscience.dataforge.misc.DfType
import space.kscience.dataforge.names.parseAsName import space.kscience.dataforge.names.parseAsName
/**
* A lifecycle state of a device
*/
@Serializable
public enum class DeviceLifecycleState {
/**
* Device is initializing
*/
STARTING,
/**
* The Device is initialized and running
*/
STARTED,
/**
* The Device is closed
*/
STOPPED,
/**
* The device encountered irrecoverable error
*/
ERROR
}
/** /**
* General interface describing a managed Device. * General interface describing a managed Device.
* [Device] is a supervisor scope encompassing all operations on a device. * [Device] is a supervisor scope encompassing all operations on a device.
* When canceled, cancels all running processes. * When canceled, cancels all running processes.
*/ */
@DfType(DEVICE_TARGET) @DfType(DEVICE_TARGET)
public interface Device : ContextAware, WithLifeCycle, CoroutineScope { public interface Device : ContextAware, CoroutineScope {
/** /**
* Initial configuration meta for the device * Initial configuration meta for the device
@ -66,16 +94,18 @@ public interface Device : ContextAware, WithLifeCycle, CoroutineScope {
* Initialize the device. This function suspends until the device is finished initialization. * Initialize the device. This function suspends until the device is finished initialization.
* Does nothing if the device is started or is starting * Does nothing if the device is started or is starting
*/ */
override suspend fun start(): Unit = Unit public suspend fun start(): Unit = Unit
/** /**
* Close and terminate the device. This function does not wait for the device to be closed. * Close and terminate the device. This function does not wait for the device to be closed.
*/ */
override suspend fun stop() { public suspend fun stop() {
coroutineContext[Job]?.cancel("The device is closed") coroutineContext[Job]?.cancel("The device is closed")
logger.info { "Device $this is closed" } logger.info { "Device $this is closed" }
} }
public val lifecycleState: DeviceLifecycleState
public companion object { public companion object {
public const val DEVICE_TARGET: String = "device" public const val DEVICE_TARGET: String = "device"
} }
@ -84,7 +114,7 @@ public interface Device : ContextAware, WithLifeCycle, CoroutineScope {
/** /**
* Inner id of a device. Not necessary corresponds to the name in the parent container * Inner id of a device. Not necessary corresponds to the name in the parent container
*/ */
public val Device.id: String get() = meta["id"].string ?: "device[${hashCode().toString(16)}]" public val Device.id: String get() = meta["id"].string?: "device[${hashCode().toString(16)}]"
/** /**
* Device that caches properties values * Device that caches properties values
@ -137,12 +167,3 @@ public fun Device.onPropertyChange(
public fun Device.propertyMessageFlow(propertyName: String): Flow<PropertyChangedMessage> = messageFlow public fun Device.propertyMessageFlow(propertyName: String): Flow<PropertyChangedMessage> = messageFlow
.filterIsInstance<PropertyChangedMessage>() .filterIsInstance<PropertyChangedMessage>()
.filter { it.property == propertyName } .filter { it.property == propertyName }
/**
* React on device lifecycle events
*/
public fun Device.onLifecycleEvent(
block: suspend (LifecycleState) -> Unit
): Job = messageFlow.filterIsInstance<DeviceLifeCycleMessage>().onEach {
block(it.state)
}.launchIn(this)

@ -240,7 +240,7 @@ public data class DeviceErrorMessage(
@Serializable @Serializable
@SerialName("lifecycle") @SerialName("lifecycle")
public data class DeviceLifeCycleMessage( public data class DeviceLifeCycleMessage(
val state: LifecycleState, val state: DeviceLifecycleState,
override val sourceDevice: Name = Name.EMPTY, override val sourceDevice: Name = Name.EMPTY,
override val targetDevice: Name? = null, override val targetDevice: Name? = null,
override val comment: String? = null, override val comment: String? = null,

@ -1,59 +0,0 @@
package space.kscience.controls.api
import kotlinx.serialization.Serializable
/**
* A lifecycle state of a device
*/
@Serializable
public enum class LifecycleState {
/**
* Device is initializing
*/
STARTING,
/**
* The Device is initialized and running
*/
STARTED,
/**
* The Device is closed
*/
STOPPED,
/**
* The device encountered irrecoverable error
*/
ERROR
}
/**
* An object that could be started or stopped functioning
*/
public interface WithLifeCycle {
public suspend fun start()
public suspend fun stop()
public val lifecycleState: LifecycleState
}
/**
* Bind this object lifecycle to a device lifecycle
*
* The starting and stopping are done in device scope
*/
public fun WithLifeCycle.bindToDeviceLifecycle(device: Device){
device.onLifecycleEvent {
when(it){
LifecycleState.STARTING -> start()
LifecycleState.STARTED -> {/*ignore*/}
LifecycleState.STOPPED -> stop()
LifecycleState.ERROR -> stop()
}
}
}

@ -6,7 +6,6 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.io.Source import kotlinx.io.Source
import space.kscience.controls.api.AsynchronousSocket import space.kscience.controls.api.AsynchronousSocket
import space.kscience.controls.api.LifecycleState
import space.kscience.dataforge.context.* import space.kscience.dataforge.context.*
import space.kscience.dataforge.meta.Meta import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.get import space.kscience.dataforge.meta.get
@ -66,8 +65,8 @@ public abstract class AbstractAsynchronousPort(
protected abstract fun onOpen() protected abstract fun onOpen()
final override suspend fun start() { final override fun open() {
if (lifecycleState == LifecycleState.STOPPED) { if (!isOpen) {
sendJob = scope.launch { sendJob = scope.launch {
for (data in outgoing) { for (data in outgoing) {
try { try {
@ -81,7 +80,7 @@ public abstract class AbstractAsynchronousPort(
} }
onOpen() onOpen()
} else { } else {
logger.warn { "$this already started" } logger.warn { "$this already opened" }
} }
} }
@ -90,7 +89,7 @@ public abstract class AbstractAsynchronousPort(
* Send a data packet via the port * Send a data packet via the port
*/ */
override suspend fun send(data: ByteArray) { override suspend fun send(data: ByteArray) {
check(lifecycleState == LifecycleState.STARTED) { "The port is not opened" } check(isOpen) { "The port is not opened" }
outgoing.send(data) outgoing.send(data)
} }
@ -101,7 +100,7 @@ public abstract class AbstractAsynchronousPort(
*/ */
override fun subscribe(): Flow<ByteArray> = incoming.receiveAsFlow() override fun subscribe(): Flow<ByteArray> = incoming.receiveAsFlow()
override suspend fun stop() { override fun close() {
outgoing.close() outgoing.close()
incoming.close() incoming.close()
sendJob?.cancel() sendJob?.cancel()

@ -7,8 +7,6 @@ import kotlinx.coroutines.sync.withLock
import kotlinx.io.Buffer import kotlinx.io.Buffer
import kotlinx.io.Source import kotlinx.io.Source
import kotlinx.io.readByteArray import kotlinx.io.readByteArray
import space.kscience.controls.api.LifecycleState
import space.kscience.controls.api.WithLifeCycle
import space.kscience.dataforge.context.Context import space.kscience.dataforge.context.Context
import space.kscience.dataforge.context.ContextAware import space.kscience.dataforge.context.ContextAware
@ -16,7 +14,11 @@ import space.kscience.dataforge.context.ContextAware
* A port handler for synchronous (request-response) communication with a port. * A port handler for synchronous (request-response) communication with a port.
* Only one request could be active at a time (others are suspended). * Only one request could be active at a time (others are suspended).
*/ */
public interface SynchronousPort : ContextAware, WithLifeCycle { public interface SynchronousPort : ContextAware, AutoCloseable {
public fun open()
public val isOpen: Boolean
/** /**
* Send a single message and wait for the flow of response chunks. * Send a single message and wait for the flow of response chunks.
@ -69,14 +71,14 @@ private class SynchronousOverAsynchronousPort(
override val context: Context get() = port.context override val context: Context get() = port.context
override suspend fun start() { override fun open() {
if (port.lifecycleState == LifecycleState.STOPPED) port.start() if (!port.isOpen) port.open()
} }
override val lifecycleState: LifecycleState get() = port.lifecycleState override val isOpen: Boolean get() = port.isOpen
override suspend fun stop() { override fun close() {
if (port.lifecycleState == LifecycleState.STARTED) port.stop() if (port.isOpen) port.close()
} }
override suspend fun <R> respond( override suspend fun <R> respond(

@ -72,6 +72,7 @@ public abstract class DeviceBase<D : Device>(
onBufferOverflow = BufferOverflow.DROP_OLDEST onBufferOverflow = BufferOverflow.DROP_OLDEST
) )
@OptIn(ExperimentalCoroutinesApi::class)
override val coroutineContext: CoroutineContext = context.newCoroutineContext( override val coroutineContext: CoroutineContext = context.newCoroutineContext(
SupervisorJob(context.coroutineContext[Job]) + SupervisorJob(context.coroutineContext[Job]) +
CoroutineName("Device $id") + CoroutineName("Device $id") +
@ -187,11 +188,11 @@ public abstract class DeviceBase<D : Device>(
return spec.executeWithMeta(self, argument ?: Meta.EMPTY) return spec.executeWithMeta(self, argument ?: Meta.EMPTY)
} }
final override var lifecycleState: LifecycleState = LifecycleState.STOPPED final override var lifecycleState: DeviceLifecycleState = DeviceLifecycleState.STOPPED
private set private set
private suspend fun setLifecycleState(lifecycleState: LifecycleState) { private suspend fun setLifecycleState(lifecycleState: DeviceLifecycleState) {
this.lifecycleState = lifecycleState this.lifecycleState = lifecycleState
sharedMessageFlow.emit( sharedMessageFlow.emit(
DeviceLifeCycleMessage(lifecycleState) DeviceLifeCycleMessage(lifecycleState)
@ -203,11 +204,11 @@ public abstract class DeviceBase<D : Device>(
} }
final override suspend fun start() { final override suspend fun start() {
if (lifecycleState == LifecycleState.STOPPED) { if (lifecycleState == DeviceLifecycleState.STOPPED) {
super.start() super.start()
setLifecycleState(LifecycleState.STARTING) setLifecycleState(DeviceLifecycleState.STARTING)
onStart() onStart()
setLifecycleState(LifecycleState.STARTED) setLifecycleState(DeviceLifecycleState.STARTED)
} else { } else {
logger.debug { "Device $this is already started" } logger.debug { "Device $this is already started" }
} }
@ -219,7 +220,7 @@ public abstract class DeviceBase<D : Device>(
final override suspend fun stop() { final override suspend fun stop() {
onStop() onStop()
setLifecycleState(LifecycleState.STOPPED) setLifecycleState(DeviceLifecycleState.STOPPED)
super.stop() super.stop()
} }

@ -33,7 +33,7 @@ public abstract class DeviceSpec<D : Device> {
public open suspend fun D.onOpen() { public open suspend fun D.onOpen() {
} }
public open suspend fun D.onClose() { public open fun D.onClose() {
} }

@ -41,24 +41,25 @@ public fun <T, D : Device> DeviceSpec<D>.mutableProperty(
write = { _, value: T -> readWriteProperty.set(this, value) } write = { _, value: T -> readWriteProperty.set(this, value) }
) )
//read only delegates
/** /**
* Register a read-only logical property * Register a mutable logical property (without a corresponding physical state) for a device
* (without a corresponding physical state or with a state that is updated asynchronously) for a device
*/ */
public fun <T, D : DeviceBase<D>> DeviceSpec<D>.property( public fun <T, D : DeviceBase<D>> DeviceSpec<D>.logicalProperty(
converter: MetaConverter<T>, converter: MetaConverter<T>,
descriptorBuilder: PropertyDescriptor.() -> Unit = {}, descriptorBuilder: PropertyDescriptor.() -> Unit = {},
name: String? = null, name: String? = null,
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DevicePropertySpec<D, T>>> = ): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, MutableDevicePropertySpec<D, T>>> =
property( mutableProperty(
converter, converter,
descriptorBuilder, descriptorBuilder,
name, name,
read = { propertyName -> getProperty(propertyName)?.let(converter::readOrNull) }, read = { propertyName -> getProperty(propertyName)?.let(converter::readOrNull) },
write = { propertyName, value -> writeProperty(propertyName, converter.convert(value)) }
) )
//read only delegates
public fun <D : Device> DeviceSpec<D>.booleanProperty( public fun <D : Device> DeviceSpec<D>.booleanProperty(
descriptorBuilder: PropertyDescriptor.() -> Unit = {}, descriptorBuilder: PropertyDescriptor.() -> Unit = {},
name: String? = null, name: String? = null,
@ -140,25 +141,7 @@ public fun <D : Device> DeviceSpec<D>.metaProperty(
//read-write delegates //read-write delegates
public fun <D : Device> DeviceSpec<D>.booleanProperty(
/**
* Register a mutable logical property
* (without a corresponding physical state or with a state that is updated asynchronously) for a device
*/
public fun <T, D : DeviceBase<D>> DeviceSpec<D>.mutableProperty(
converter: MetaConverter<T>,
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
name: String? = null,
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, MutableDevicePropertySpec<D, T>>> =
mutableProperty(
converter,
descriptorBuilder,
name,
read = { propertyName -> getProperty(propertyName)?.let(converter::readOrNull) },
write = { propertyName, value -> writeProperty(propertyName, converter.convert(value)) }
)
public fun <D : Device> DeviceSpec<D>.mutableBooleanProperty(
descriptorBuilder: PropertyDescriptor.() -> Unit = {}, descriptorBuilder: PropertyDescriptor.() -> Unit = {},
name: String? = null, name: String? = null,
read: suspend D.(propertyName: String) -> Boolean?, read: suspend D.(propertyName: String) -> Boolean?,
@ -178,7 +161,7 @@ public fun <D : Device> DeviceSpec<D>.mutableBooleanProperty(
) )
public fun <D : Device> DeviceSpec<D>.mutableNumberProperty( public fun <D : Device> DeviceSpec<D>.numberProperty(
descriptorBuilder: PropertyDescriptor.() -> Unit = {}, descriptorBuilder: PropertyDescriptor.() -> Unit = {},
name: String? = null, name: String? = null,
read: suspend D.(propertyName: String) -> Number, read: suspend D.(propertyName: String) -> Number,
@ -186,7 +169,7 @@ public fun <D : Device> DeviceSpec<D>.mutableNumberProperty(
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, MutableDevicePropertySpec<D, Number>>> = ): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, MutableDevicePropertySpec<D, Number>>> =
mutableProperty(MetaConverter.number, numberDescriptor(descriptorBuilder), name, read, write) mutableProperty(MetaConverter.number, numberDescriptor(descriptorBuilder), name, read, write)
public fun <D : Device> DeviceSpec<D>.mutableDoubleProperty( public fun <D : Device> DeviceSpec<D>.doubleProperty(
descriptorBuilder: PropertyDescriptor.() -> Unit = {}, descriptorBuilder: PropertyDescriptor.() -> Unit = {},
name: String? = null, name: String? = null,
read: suspend D.(propertyName: String) -> Double, read: suspend D.(propertyName: String) -> Double,
@ -194,7 +177,7 @@ public fun <D : Device> DeviceSpec<D>.mutableDoubleProperty(
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, MutableDevicePropertySpec<D, Double>>> = ): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, MutableDevicePropertySpec<D, Double>>> =
mutableProperty(MetaConverter.double, numberDescriptor(descriptorBuilder), name, read, write) mutableProperty(MetaConverter.double, numberDescriptor(descriptorBuilder), name, read, write)
public fun <D : Device> DeviceSpec<D>.mutableStringProperty( public fun <D : Device> DeviceSpec<D>.stringProperty(
descriptorBuilder: PropertyDescriptor.() -> Unit = {}, descriptorBuilder: PropertyDescriptor.() -> Unit = {},
name: String? = null, name: String? = null,
read: suspend D.(propertyName: String) -> String, read: suspend D.(propertyName: String) -> String,
@ -202,7 +185,7 @@ public fun <D : Device> DeviceSpec<D>.mutableStringProperty(
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, MutableDevicePropertySpec<D, String>>> = ): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, MutableDevicePropertySpec<D, String>>> =
mutableProperty(MetaConverter.string, descriptorBuilder, name, read, write) mutableProperty(MetaConverter.string, descriptorBuilder, name, read, write)
public fun <D : Device> DeviceSpec<D>.mutableMetaProperty( public fun <D : Device> DeviceSpec<D>.metaProperty(
descriptorBuilder: PropertyDescriptor.() -> Unit = {}, descriptorBuilder: PropertyDescriptor.() -> Unit = {},
name: String? = null, name: String? = null,
read: suspend D.(propertyName: String) -> Meta, read: suspend D.(propertyName: String) -> Meta,

@ -1,7 +1,6 @@
package space.kscience.controls.ports package space.kscience.controls.ports
import kotlinx.coroutines.* import kotlinx.coroutines.*
import space.kscience.controls.api.LifecycleState
import space.kscience.dataforge.context.* import space.kscience.dataforge.context.*
import space.kscience.dataforge.meta.* import space.kscience.dataforge.meta.*
import java.net.InetSocketAddress import java.net.InetSocketAddress
@ -31,7 +30,7 @@ public class ChannelPort(
meta: Meta, meta: Meta,
coroutineContext: CoroutineContext = context.coroutineContext, coroutineContext: CoroutineContext = context.coroutineContext,
channelBuilder: suspend () -> ByteChannel, channelBuilder: suspend () -> ByteChannel,
) : AbstractAsynchronousPort(context, meta, coroutineContext) { ) : AbstractAsynchronousPort(context, meta, coroutineContext), AutoCloseable {
/** /**
* A handler to await port connection * A handler to await port connection
@ -42,8 +41,7 @@ public class ChannelPort(
private var listenerJob: Job? = null private var listenerJob: Job? = null
override val lifecycleState: LifecycleState override val isOpen: Boolean get() = listenerJob?.isActive == true
get() = if(listenerJob?.isActive == true) LifecycleState.STARTED else LifecycleState.STOPPED
override fun onOpen() { override fun onOpen() {
listenerJob = scope.launch(Dispatchers.IO) { listenerJob = scope.launch(Dispatchers.IO) {
@ -73,12 +71,12 @@ public class ChannelPort(
} }
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
override suspend fun stop() { override fun close() {
listenerJob?.cancel() listenerJob?.cancel()
if (futureChannel.isCompleted) { if (futureChannel.isCompleted) {
futureChannel.getCompleted().close() futureChannel.getCompleted().close()
} }
super.stop() super.close()
} }
} }
@ -107,12 +105,12 @@ public object TcpPort : Factory<AsynchronousPort> {
/** /**
* Create and open TCP port * Create and open TCP port
*/ */
public suspend fun start( public fun open(
context: Context, context: Context,
host: String, host: String,
port: Int, port: Int,
coroutineContext: CoroutineContext = context.coroutineContext, coroutineContext: CoroutineContext = context.coroutineContext,
): ChannelPort = build(context, host, port, coroutineContext).apply { start() } ): ChannelPort = build(context, host, port, coroutineContext).apply { open() }
override fun build(context: Context, meta: Meta): ChannelPort { override fun build(context: Context, meta: Meta): ChannelPort {
val host = meta["host"].string ?: "localhost" val host = meta["host"].string ?: "localhost"
@ -158,13 +156,13 @@ public object UdpPort : Factory<AsynchronousPort> {
/** /**
* Connect a datagram channel to a remote host/port. If [localPort] is provided, it is used to bind local port for receiving messages. * Connect a datagram channel to a remote host/port. If [localPort] is provided, it is used to bind local port for receiving messages.
*/ */
public suspend fun start( public fun open(
context: Context, context: Context,
remoteHost: String, remoteHost: String,
remotePort: Int, remotePort: Int,
localPort: Int? = null, localPort: Int? = null,
localHost: String = "localhost", localHost: String = "localhost",
): ChannelPort = build(context, remoteHost, remotePort, localPort, localHost).apply { start() } ): ChannelPort = build(context, remoteHost, remotePort, localPort, localHost).apply { open() }
override fun build(context: Context, meta: Meta): ChannelPort { override fun build(context: Context, meta: Meta): ChannelPort {

@ -1,7 +1,6 @@
package space.kscience.controls.ports package space.kscience.controls.ports
import kotlinx.coroutines.* import kotlinx.coroutines.*
import space.kscience.controls.api.LifecycleState
import space.kscience.dataforge.context.Context import space.kscience.dataforge.context.Context
import space.kscience.dataforge.meta.Meta import space.kscience.dataforge.meta.Meta
import java.net.DatagramPacket import java.net.DatagramPacket
@ -40,13 +39,13 @@ public class UdpSocketPort(
} }
} }
override suspend fun stop() { override fun close() {
listenerJob?.cancel() listenerJob?.cancel()
super.stop() super.close()
} }
override val lifecycleState: LifecycleState override val isOpen: Boolean get() = listenerJob?.isActive == true
get() = if(listenerJob?.isActive == true) LifecycleState.STARTED else LifecycleState.STOPPED
override suspend fun write(data: ByteArray): Unit = withContext(Dispatchers.IO) { override suspend fun write(data: ByteArray): Unit = withContext(Dispatchers.IO) {
val packet = DatagramPacket( val packet = DatagramPacket(

@ -29,8 +29,8 @@ internal class AsynchronousPortIOTest {
@Test @Test
fun testUdpCommunication() = runTest { fun testUdpCommunication() = runTest {
val receiver = UdpPort.start(Global, "localhost", 8811, localPort = 8812) val receiver = UdpPort.open(Global, "localhost", 8811, localPort = 8812)
val sender = UdpPort.start(Global, "localhost", 8812, localPort = 8811) val sender = UdpPort.open(Global, "localhost", 8812, localPort = 8811)
delay(30) delay(30)
repeat(10) { repeat(10) {
@ -44,7 +44,7 @@ internal class AsynchronousPortIOTest {
.toList() .toList()
assertEquals("Line number 3", res[3].trim()) assertEquals("Line number 3", res[3].trim())
receiver.stop() receiver.close()
sender.stop() sender.close()
} }
} }

@ -1,9 +0,0 @@
package space.kscience.controls.spec
import space.kscience.controls.api.ActionDescriptor
import space.kscience.controls.api.PropertyDescriptor
import kotlin.reflect.KProperty
internal actual fun PropertyDescriptor.fromSpec(property: KProperty<*>){}
internal actual fun ActionDescriptor.fromSpec(property: KProperty<*>){}

@ -6,7 +6,7 @@
## Artifact: ## Artifact:
The Maven coordinates of this project are `space.kscience:controls-jupyter:0.4.0-dev-7`. The Maven coordinates of this project are `space.kscience:controls-jupyter:0.4.0-dev-4`.
**Gradle Kotlin DSL:** **Gradle Kotlin DSL:**
```kotlin ```kotlin
@ -16,6 +16,6 @@ repositories {
} }
dependencies { dependencies {
implementation("space.kscience:controls-jupyter:0.4.0-dev-7") implementation("space.kscience:controls-jupyter:0.4.0-dev-4")
} }
``` ```

@ -12,7 +12,7 @@ Magix service for binding controls devices (both as RPC client and server)
## Artifact: ## Artifact:
The Maven coordinates of this project are `space.kscience:controls-magix:0.4.0-dev-7`. The Maven coordinates of this project are `space.kscience:controls-magix:0.4.0-dev-4`.
**Gradle Kotlin DSL:** **Gradle Kotlin DSL:**
```kotlin ```kotlin
@ -22,6 +22,6 @@ repositories {
} }
dependencies { dependencies {
implementation("space.kscience:controls-magix:0.4.0-dev-7") implementation("space.kscience:controls-magix:0.4.0-dev-4")
} }
``` ```

@ -13,7 +13,6 @@ kscience {
jvm() jvm()
js() js()
native() native()
wasm()
useCoroutines() useCoroutines()
useSerialization { useSerialization {
json() json()

@ -99,10 +99,10 @@ public class DeviceClient internal constructor(
} }
private val lifecycleStateFlow = messageFlow.filterIsInstance<DeviceLifeCycleMessage>() private val lifecycleStateFlow = messageFlow.filterIsInstance<DeviceLifeCycleMessage>()
.map { it.state }.stateIn(this, started = SharingStarted.Eagerly, LifecycleState.STARTED) .map { it.state }.stateIn(this, started = SharingStarted.Eagerly, DeviceLifecycleState.STARTED)
@DFExperimental @DFExperimental
override val lifecycleState: LifecycleState get() = lifecycleStateFlow.value override val lifecycleState: DeviceLifecycleState get() = lifecycleStateFlow.value
} }
/** /**

@ -14,7 +14,7 @@ Automatically checks consistency.
## Artifact: ## Artifact:
The Maven coordinates of this project are `space.kscience:controls-modbus:0.4.0-dev-7`. The Maven coordinates of this project are `space.kscience:controls-modbus:0.4.0-dev-4`.
**Gradle Kotlin DSL:** **Gradle Kotlin DSL:**
```kotlin ```kotlin
@ -24,6 +24,6 @@ repositories {
} }
dependencies { dependencies {
implementation("space.kscience:controls-modbus:0.4.0-dev-7") implementation("space.kscience:controls-modbus:0.4.0-dev-4")
} }
``` ```

@ -12,7 +12,7 @@ A client and server connectors for OPC-UA via Eclipse Milo
## Artifact: ## Artifact:
The Maven coordinates of this project are `space.kscience:controls-opcua:0.4.0-dev-7`. The Maven coordinates of this project are `space.kscience:controls-opcua:0.4.0-dev-4`.
**Gradle Kotlin DSL:** **Gradle Kotlin DSL:**
```kotlin ```kotlin
@ -22,6 +22,6 @@ repositories {
} }
dependencies { dependencies {
implementation("space.kscience:controls-opcua:0.4.0-dev-7") implementation("space.kscience:controls-opcua:0.4.0-dev-4")
} }
``` ```

@ -6,7 +6,7 @@ Utils to work with controls-kt on Raspberry pi
## Artifact: ## Artifact:
The Maven coordinates of this project are `space.kscience:controls-pi:0.4.0-dev-7`. The Maven coordinates of this project are `space.kscience:controls-pi:0.4.0-dev-4`.
**Gradle Kotlin DSL:** **Gradle Kotlin DSL:**
```kotlin ```kotlin
@ -16,6 +16,6 @@ repositories {
} }
dependencies { dependencies {
implementation("space.kscience:controls-pi:0.4.0-dev-7") implementation("space.kscience:controls-pi:0.4.0-dev-4")
} }
``` ```

@ -5,7 +5,6 @@ import com.pi4j.io.serial.Serial
import com.pi4j.io.serial.SerialConfigBuilder import com.pi4j.io.serial.SerialConfigBuilder
import com.pi4j.ktx.io.serial import com.pi4j.ktx.io.serial
import kotlinx.coroutines.* import kotlinx.coroutines.*
import space.kscience.controls.api.LifecycleState
import space.kscience.controls.ports.AbstractAsynchronousPort import space.kscience.controls.ports.AbstractAsynchronousPort
import space.kscience.controls.ports.AsynchronousPort import space.kscience.controls.ports.AsynchronousPort
import space.kscience.controls.ports.copyToArray import space.kscience.controls.ports.copyToArray
@ -50,10 +49,9 @@ public class AsynchronousPiPort(
} }
override val lifecycleState: LifecycleState override val isOpen: Boolean get() = listenerJob?.isActive == true
get() = if(listenerJob?.isActive == true) LifecycleState.STARTED else LifecycleState.STOPPED
override suspend fun stop() { override fun close() {
listenerJob?.cancel() listenerJob?.cancel()
serial.close() serial.close()
} }
@ -76,11 +74,11 @@ public class AsynchronousPiPort(
return AsynchronousPiPort(context, meta, serial) return AsynchronousPiPort(context, meta, serial)
} }
public suspend fun start( public fun open(
context: Context, context: Context,
device: String, device: String,
block: SerialConfigBuilder.() -> Unit, block: SerialConfigBuilder.() -> Unit,
): AsynchronousPiPort = build(context, device, block).apply { start() } ): AsynchronousPiPort = build(context, device, block).apply { open() }
override fun build(context: Context, meta: Meta): AsynchronousPort { override fun build(context: Context, meta: Meta): AsynchronousPort {
val device: String = meta["device"].string ?: error("Device name not defined") val device: String = meta["device"].string ?: error("Device name not defined")

@ -10,7 +10,6 @@ import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.runInterruptible import kotlinx.coroutines.runInterruptible
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
import space.kscience.controls.api.LifecycleState
import space.kscience.controls.ports.SynchronousPort import space.kscience.controls.ports.SynchronousPort
import space.kscience.controls.ports.copyToArray import space.kscience.controls.ports.copyToArray
import space.kscience.dataforge.context.* import space.kscience.dataforge.context.*
@ -28,13 +27,11 @@ public class SynchronousPiPort(
) : SynchronousPort { ) : SynchronousPort {
private val pi = context.request(PiPlugin) private val pi = context.request(PiPlugin)
override fun open() {
override suspend fun start() {
serial.open() serial.open()
} }
override val lifecycleState: LifecycleState override val isOpen: Boolean get() = serial.isOpen
get() = if(serial.isOpen) LifecycleState.STARTED else LifecycleState.STOPPED
override suspend fun <R> respond( override suspend fun <R> respond(
request: ByteArray, request: ByteArray,
@ -44,7 +41,7 @@ public class SynchronousPiPort(
serial.write(request) serial.write(request)
flow<ByteArray> { flow<ByteArray> {
val buffer = ByteBuffer.allocate(1024) val buffer = ByteBuffer.allocate(1024)
while (serial.isOpen) { while (isOpen) {
try { try {
val num = serial.read(buffer) val num = serial.read(buffer)
if (num > 0) { if (num > 0) {
@ -67,7 +64,7 @@ public class SynchronousPiPort(
} }
} }
override suspend fun stop() { override fun close() {
serial.close() serial.close()
} }
@ -89,11 +86,11 @@ public class SynchronousPiPort(
return SynchronousPiPort(context, meta, serial) return SynchronousPiPort(context, meta, serial)
} }
public suspend fun start( public fun open(
context: Context, context: Context,
device: String, device: String,
block: SerialConfigBuilder.() -> Unit, block: SerialConfigBuilder.() -> Unit,
): SynchronousPiPort = build(context, device, block).apply { start() } ): SynchronousPiPort = build(context, device, block).apply { open() }
override fun build(context: Context, meta: Meta): SynchronousPiPort { override fun build(context: Context, meta: Meta): SynchronousPiPort {
val device: String = meta["device"].string ?: error("Device name not defined") val device: String = meta["device"].string ?: error("Device name not defined")

@ -1,21 +0,0 @@
# Module controls-plc4x
A plugin for Controls-kt device server on top of plc4x library
## Usage
## Artifact:
The Maven coordinates of this project are `space.kscience:controls-plc4x:0.4.0-dev-7`.
**Gradle Kotlin DSL:**
```kotlin
repositories {
maven("https://repo.kotlin.link")
mavenCentral()
}
dependencies {
implementation("space.kscience:controls-plc4x:0.4.0-dev-7")
}
```

@ -6,7 +6,7 @@ Implementation of byte ports on top os ktor-io asynchronous API
## Artifact: ## Artifact:
The Maven coordinates of this project are `space.kscience:controls-ports-ktor:0.4.0-dev-7`. The Maven coordinates of this project are `space.kscience:controls-ports-ktor:0.4.0-dev-4`.
**Gradle Kotlin DSL:** **Gradle Kotlin DSL:**
```kotlin ```kotlin
@ -16,6 +16,6 @@ repositories {
} }
dependencies { dependencies {
implementation("space.kscience:controls-ports-ktor:0.4.0-dev-7") implementation("space.kscience:controls-ports-ktor:0.4.0-dev-4")
} }
``` ```

@ -6,9 +6,9 @@ import io.ktor.network.sockets.aSocket
import io.ktor.network.sockets.openReadChannel import io.ktor.network.sockets.openReadChannel
import io.ktor.network.sockets.openWriteChannel import io.ktor.network.sockets.openWriteChannel
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.* import kotlinx.coroutines.*
import space.kscience.controls.api.LifecycleState
import space.kscience.dataforge.context.Context import space.kscience.dataforge.context.Context
import space.kscience.dataforge.context.Factory import space.kscience.dataforge.context.Factory
import space.kscience.dataforge.meta.Meta import space.kscience.dataforge.meta.Meta
@ -25,7 +25,7 @@ public class KtorTcpPort internal constructor(
public val port: Int, public val port: Int,
coroutineContext: CoroutineContext = context.coroutineContext, coroutineContext: CoroutineContext = context.coroutineContext,
socketOptions: SocketOptions.TCPClientSocketOptions.() -> Unit = {}, socketOptions: SocketOptions.TCPClientSocketOptions.() -> Unit = {},
) : AbstractAsynchronousPort(context, meta, coroutineContext) { ) : AbstractAsynchronousPort(context, meta, coroutineContext), Closeable {
override fun toString(): String = "port[tcp:$host:$port]" override fun toString(): String = "port[tcp:$host:$port]"
@ -55,13 +55,13 @@ public class KtorTcpPort internal constructor(
writeChannel.await().writeAvailable(data) writeChannel.await().writeAvailable(data)
} }
override val lifecycleState: LifecycleState override val isOpen: Boolean
get() = if(listenerJob?.isActive == true) LifecycleState.STARTED else LifecycleState.STOPPED get() = listenerJob?.isActive == true
override suspend fun stop() { override fun close() {
listenerJob?.cancel() listenerJob?.cancel()
futureSocket.cancel() futureSocket.cancel()
super.stop() super.close()
} }
public companion object : Factory<AsynchronousPort> { public companion object : Factory<AsynchronousPort> {
@ -82,13 +82,13 @@ public class KtorTcpPort internal constructor(
return KtorTcpPort(context, meta, host, port, coroutineContext, socketOptions) return KtorTcpPort(context, meta, host, port, coroutineContext, socketOptions)
} }
public suspend fun start( public fun open(
context: Context, context: Context,
host: String, host: String,
port: Int, port: Int,
coroutineContext: CoroutineContext = context.coroutineContext, coroutineContext: CoroutineContext = context.coroutineContext,
socketOptions: SocketOptions.TCPClientSocketOptions.() -> Unit = {}, socketOptions: SocketOptions.TCPClientSocketOptions.() -> Unit = {},
): KtorTcpPort = build(context, host, port, coroutineContext, socketOptions).apply { start() } ): KtorTcpPort = build(context, host, port, coroutineContext, socketOptions).apply { open() }
override fun build(context: Context, meta: Meta): AsynchronousPort { override fun build(context: Context, meta: Meta): AsynchronousPort {
val host = meta["host"].string ?: "localhost" val host = meta["host"].string ?: "localhost"

@ -4,9 +4,9 @@ import io.ktor.network.selector.ActorSelectorManager
import io.ktor.network.sockets.* import io.ktor.network.sockets.*
import io.ktor.utils.io.ByteWriteChannel import io.ktor.utils.io.ByteWriteChannel
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.* import kotlinx.coroutines.*
import space.kscience.controls.api.LifecycleState
import space.kscience.dataforge.context.Context import space.kscience.dataforge.context.Context
import space.kscience.dataforge.context.Factory import space.kscience.dataforge.context.Factory
import space.kscience.dataforge.meta.Meta import space.kscience.dataforge.meta.Meta
@ -24,7 +24,7 @@ public class KtorUdpPort internal constructor(
public val localHost: String = "localhost", public val localHost: String = "localhost",
coroutineContext: CoroutineContext = context.coroutineContext, coroutineContext: CoroutineContext = context.coroutineContext,
socketOptions: SocketOptions.UDPSocketOptions.() -> Unit = {}, socketOptions: SocketOptions.UDPSocketOptions.() -> Unit = {},
) : AbstractAsynchronousPort(context, meta, coroutineContext) { ) : AbstractAsynchronousPort(context, meta, coroutineContext), Closeable {
override fun toString(): String = "port[udp:$remoteHost:$remotePort]" override fun toString(): String = "port[udp:$remoteHost:$remotePort]"
@ -58,13 +58,13 @@ public class KtorUdpPort internal constructor(
writeChannel.await().writeAvailable(data) writeChannel.await().writeAvailable(data)
} }
override val lifecycleState: LifecycleState override val isOpen: Boolean
get() = if(listenerJob?.isActive == true) LifecycleState.STARTED else LifecycleState.STOPPED get() = listenerJob?.isActive == true
override suspend fun stop() { override fun close() {
listenerJob?.cancel() listenerJob?.cancel()
futureSocket.cancel() futureSocket.cancel()
super.stop() super.close()
} }
public companion object : Factory<AsynchronousPort> { public companion object : Factory<AsynchronousPort> {
@ -101,7 +101,7 @@ public class KtorUdpPort internal constructor(
/** /**
* Create and open UDP port * Create and open UDP port
*/ */
public suspend fun start( public fun open(
context: Context, context: Context,
remoteHost: String, remoteHost: String,
remotePort: Int, remotePort: Int,
@ -117,7 +117,7 @@ public class KtorUdpPort internal constructor(
localHost, localHost,
coroutineContext, coroutineContext,
socketOptions socketOptions
).apply { start() } ).apply { open() }
override fun build(context: Context, meta: Meta): AsynchronousPort { override fun build(context: Context, meta: Meta): AsynchronousPort {
val remoteHost by meta.string { error("Remote host is not specified") } val remoteHost by meta.string { error("Remote host is not specified") }

@ -6,7 +6,7 @@ Implementation of direct serial port communication with JSerialComm
## Artifact: ## Artifact:
The Maven coordinates of this project are `space.kscience:controls-serial:0.4.0-dev-7`. The Maven coordinates of this project are `space.kscience:controls-serial:0.4.0-dev-4`.
**Gradle Kotlin DSL:** **Gradle Kotlin DSL:**
```kotlin ```kotlin
@ -16,6 +16,6 @@ repositories {
} }
dependencies { dependencies {
implementation("space.kscience:controls-serial:0.4.0-dev-7") implementation("space.kscience:controls-serial:0.4.0-dev-4")
} }
``` ```

@ -5,7 +5,6 @@ import com.fazecast.jSerialComm.SerialPortDataListener
import com.fazecast.jSerialComm.SerialPortEvent import com.fazecast.jSerialComm.SerialPortEvent
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import space.kscience.controls.api.LifecycleState
import space.kscience.controls.ports.AbstractAsynchronousPort import space.kscience.controls.ports.AbstractAsynchronousPort
import space.kscience.controls.ports.AsynchronousPort import space.kscience.controls.ports.AsynchronousPort
import space.kscience.dataforge.context.Context import space.kscience.dataforge.context.Context
@ -29,7 +28,7 @@ public class AsynchronousSerialPort(
private val serialPortListener = object : SerialPortDataListener { private val serialPortListener = object : SerialPortDataListener {
override fun getListeningEvents(): Int = override fun getListeningEvents(): Int =
SerialPort.LISTENING_EVENT_DATA_RECEIVED or SerialPort.LISTENING_EVENT_DATA_AVAILABLE SerialPort.LISTENING_EVENT_DATA_RECEIVED and SerialPort.LISTENING_EVENT_DATA_AVAILABLE
override fun serialEvent(event: SerialPortEvent) { override fun serialEvent(event: SerialPortEvent) {
when (event.eventType) { when (event.eventType) {
@ -56,20 +55,18 @@ public class AsynchronousSerialPort(
comPort.addDataListener(serialPortListener) comPort.addDataListener(serialPortListener)
} }
override val lifecycleState: LifecycleState override val isOpen: Boolean get() = comPort.isOpen
get() = if(comPort.isOpen) LifecycleState.STARTED else LifecycleState.STOPPED
override suspend fun write(data: ByteArray) { override suspend fun write(data: ByteArray) {
comPort.writeBytes(data, data.size) comPort.writeBytes(data, data.size)
} }
override suspend fun stop() { override fun close() {
comPort.removeDataListener() comPort.removeDataListener()
if (comPort.isOpen) { if (comPort.isOpen) {
comPort.closePort() comPort.closePort()
} }
super.stop() super.close()
} }
public companion object : Factory<AsynchronousPort> { public companion object : Factory<AsynchronousPort> {
@ -103,7 +100,7 @@ public class AsynchronousSerialPort(
/** /**
* Construct ComPort with given parameters * Construct ComPort with given parameters
*/ */
public suspend fun start( public fun open(
context: Context, context: Context,
portName: String, portName: String,
baudRate: Int = 9600, baudRate: Int = 9600,
@ -121,7 +118,7 @@ public class AsynchronousSerialPort(
parity = parity, parity = parity,
coroutineContext = coroutineContext, coroutineContext = coroutineContext,
additionalConfig = additionalConfig additionalConfig = additionalConfig
).apply { start() } ).apply { open() }
override fun build(context: Context, meta: Meta): AsynchronousPort { override fun build(context: Context, meta: Meta): AsynchronousPort {

@ -11,8 +11,6 @@ import space.kscience.dataforge.names.asName
public class SerialPortPlugin : AbstractPlugin() { public class SerialPortPlugin : AbstractPlugin() {
public val ports: Ports by require(Ports)
override val tag: PluginTag get() = Companion.tag override val tag: PluginTag get() = Companion.tag
override fun content(target: String): Map<Name, Any> = when (target) { override fun content(target: String): Map<Name, Any> = when (target) {

@ -7,7 +7,6 @@ import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.runInterruptible import kotlinx.coroutines.runInterruptible
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
import space.kscience.controls.api.LifecycleState
import space.kscience.controls.ports.SynchronousPort import space.kscience.controls.ports.SynchronousPort
import space.kscience.dataforge.context.Context import space.kscience.dataforge.context.Context
import space.kscience.dataforge.context.Factory import space.kscience.dataforge.context.Factory
@ -29,17 +28,16 @@ public class SynchronousSerialPort(
override fun toString(): String = "port[${comPort.descriptivePortName}]" override fun toString(): String = "port[${comPort.descriptivePortName}]"
override suspend fun start() { override fun open() {
if (!comPort.isOpen) { if (!isOpen) {
comPort.openPort() comPort.openPort()
} }
} }
override val lifecycleState: LifecycleState override val isOpen: Boolean get() = comPort.isOpen
get() = if(comPort.isOpen) LifecycleState.STARTED else LifecycleState.STOPPED
override suspend fun stop() { override fun close() {
if (comPort.isOpen) { if (comPort.isOpen) {
comPort.closePort() comPort.closePort()
} }
@ -54,7 +52,7 @@ public class SynchronousSerialPort(
comPort.flushIOBuffers() comPort.flushIOBuffers()
comPort.writeBytes(request, request.size) comPort.writeBytes(request, request.size)
flow<ByteArray> { flow<ByteArray> {
while (comPort.isOpen) { while (isOpen) {
try { try {
val available = comPort.bytesAvailable() val available = comPort.bytesAvailable()
if (available > 0) { if (available > 0) {
@ -110,7 +108,7 @@ public class SynchronousSerialPort(
/** /**
* Construct ComPort with given parameters * Construct ComPort with given parameters
*/ */
public suspend fun start( public fun open(
context: Context, context: Context,
portName: String, portName: String,
baudRate: Int = 9600, baudRate: Int = 9600,
@ -126,7 +124,7 @@ public class SynchronousSerialPort(
stopBits = stopBits, stopBits = stopBits,
parity = parity, parity = parity,
additionalConfig = additionalConfig additionalConfig = additionalConfig
).apply { start() } ).apply { open() }
override fun build(context: Context, meta: Meta): SynchronousPort { override fun build(context: Context, meta: Meta): SynchronousPort {

@ -6,7 +6,7 @@ A combined Magix event loop server with web server for visualization.
## Artifact: ## Artifact:
The Maven coordinates of this project are `space.kscience:controls-server:0.4.0-dev-7`. The Maven coordinates of this project are `space.kscience:controls-server:0.4.0-dev-4`.
**Gradle Kotlin DSL:** **Gradle Kotlin DSL:**
```kotlin ```kotlin
@ -16,6 +16,6 @@ repositories {
} }
dependencies { dependencies {
implementation("space.kscience:controls-server:0.4.0-dev-7") implementation("space.kscience:controls-server:0.4.0-dev-4")
} }
``` ```

@ -6,7 +6,7 @@ An API for stand-alone Controls-kt device or a hub.
## Artifact: ## Artifact:
The Maven coordinates of this project are `space.kscience:controls-storage:0.4.0-dev-7`. The Maven coordinates of this project are `space.kscience:controls-storage:0.4.0-dev-4`.
**Gradle Kotlin DSL:** **Gradle Kotlin DSL:**
```kotlin ```kotlin
@ -16,6 +16,6 @@ repositories {
} }
dependencies { dependencies {
implementation("space.kscience:controls-storage:0.4.0-dev-7") implementation("space.kscience:controls-storage:0.4.0-dev-4")
} }
``` ```

@ -6,7 +6,7 @@ An implementation of controls-storage on top of JetBrains Xodus.
## Artifact: ## Artifact:
The Maven coordinates of this project are `space.kscience:controls-xodus:0.4.0-dev-7`. The Maven coordinates of this project are `space.kscience:controls-xodus:0.4.0-dev-4`.
**Gradle Kotlin DSL:** **Gradle Kotlin DSL:**
```kotlin ```kotlin
@ -16,6 +16,6 @@ repositories {
} }
dependencies { dependencies {
implementation("space.kscience:controls-xodus:0.4.0-dev-7") implementation("space.kscience:controls-xodus:0.4.0-dev-4")
} }
``` ```

@ -6,7 +6,7 @@ Dashboard and visualization extensions for devices
## Artifact: ## Artifact:
The Maven coordinates of this project are `space.kscience:controls-vision:0.4.0-dev-7`. The Maven coordinates of this project are `space.kscience:controls-vision:0.4.0-dev-4`.
**Gradle Kotlin DSL:** **Gradle Kotlin DSL:**
```kotlin ```kotlin
@ -16,6 +16,6 @@ repositories {
} }
dependencies { dependencies {
implementation("space.kscience:controls-vision:0.4.0-dev-7") implementation("space.kscience:controls-vision:0.4.0-dev-4")
} }
``` ```

@ -1,21 +0,0 @@
# Module controls-visualisation-compose
Visualisation extension using compose-multiplatform
## Usage
## Artifact:
The Maven coordinates of this project are `space.kscience:controls-visualisation-compose:0.4.0-dev-7`.
**Gradle Kotlin DSL:**
```kotlin
repositories {
maven("https://repo.kotlin.link")
mavenCentral()
}
dependencies {
implementation("space.kscience:controls-visualisation-compose:0.4.0-dev-7")
}
```

@ -18,11 +18,25 @@ kscience {
useContextReceivers() useContextReceivers()
commonMain { commonMain {
api(projects.controlsConstructor) api(projects.controlsConstructor)
api(libs.koala.plots) api("io.github.koalaplot:koalaplot-core:0.6.0")
api(compose.foundation) }
api(compose.material3) }
@OptIn(ExperimentalComposeLibrary::class)
api(compose.desktop.components.splitPane) kotlin {
sourceSets {
commonMain {
dependencies {
api(compose.foundation)
api(compose.material3)
@OptIn(ExperimentalComposeLibrary::class)
api(compose.desktop.components.splitPane)
}
}
// jvmMain {
// dependencies {
// implementation(compose.desktop.currentOs)
// }
// }
} }
} }

@ -1,64 +0,0 @@
package space.kscience.controls.compose
import androidx.compose.runtime.Immutable
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.graphics.drawscope.rotate
/**
* A single 2D drawable
*/
@Immutable
public sealed interface DeviceDrawable2D {
public fun DrawScope.draw()
override fun equals(other: Any?): Boolean
}
@Immutable
public data class CircleDrawable2D(val position: Offset, val radius: Float, val color: Color) : DeviceDrawable2D {
override fun DrawScope.draw() {
drawCircle(color, radius = radius, center = position)
}
}
@Drawable2DBuilder
public fun DeviceDrawable2DStore.circle(id: String, position: Offset, radius: Float, color: Color) {
emit(id, CircleDrawable2D(position, radius, color))
}
@Immutable
public data class RectangleDrawable2D(
val position: Offset,
val rectangleSize: Size,
val color: Color,
val rotateDegrees: Float = 0f,
) : DeviceDrawable2D {
override fun DrawScope.draw() {
rotate(rotateDegrees) {
drawRect(
color = color,
topLeft = Offset(
(position.x - rectangleSize.width / 2),
(position.y - rectangleSize.height / 2)
),
size = Size(rectangleSize.width, rectangleSize.height)
)
}
}
}
@Drawable2DBuilder
public fun DeviceDrawable2DStore.rectangle(
id: String,
position: Offset,
rectangleSize: Size,
color: Color,
rotateDegrees: Float = 0f,
) {
emit(id, RectangleDrawable2D(position, rectangleSize, color, rotateDegrees))
}

@ -1,94 +0,0 @@
package space.kscience.controls.compose
import androidx.compose.foundation.Canvas
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.graphics.drawscope.clipRect
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.unit.toSize
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import space.kscience.controls.api.Device
import space.kscience.controls.constructor.DeviceState
import space.kscience.controls.spec.DevicePropertySpec
import space.kscience.controls.spec.propertyFlow
@DslMarker
public annotation class Drawable2DBuilder
@Drawable2DBuilder
public class DeviceDrawable2DStore(public val scope: CoroutineScope, public val size: Size) {
public val drawableFlow: MutableStateFlow<Map<String, DeviceDrawable2D>> = MutableStateFlow(emptyMap())
}
public fun DeviceDrawable2DStore.emit(id: String, drawable2D: DeviceDrawable2D) {
drawableFlow.value += (id to drawable2D)
}
public fun DeviceDrawable2DStore.emitAll(drawables: Map<String, DeviceDrawable2D>) {
drawableFlow.value += drawables
}
/**
* Fill drawables from a flow
*/
public fun DeviceDrawable2DStore.observe(id: String, flow: Flow<DeviceDrawable2D>): Job = flow.onEach {
drawableFlow.value += (id to it)
}.launchIn(scope)
/**
* Observe single [DeviceState]
*/
public fun <T> DeviceDrawable2DStore.observeState(
state: DeviceState<T>,
id: String = state.toString(),
transform: suspend DeviceDrawable2DStore.(T) -> DeviceDrawable2D,
): Job = observe(id, state.valueFlow.map { transform(this, it) })
/**
* Observe a single [Device] property
*/
public fun <T, D : Device, P : DevicePropertySpec<D, T>> DeviceDrawable2DStore.observeProperty(
device: D,
devicePropertySpec: DevicePropertySpec<D, T>,
id: String = devicePropertySpec.toString(),
transform: suspend DeviceDrawable2DStore.(T) -> DeviceDrawable2D,
): Job = observe(id, device.propertyFlow(devicePropertySpec).map { transform(this, it) })
@Composable
public fun Device2DCanvas(
modifier: Modifier = Modifier,
onDraw: DrawScope.() -> Unit = {},
flowBuilder: suspend DeviceDrawable2DStore.() -> Unit,
) {
val coroutineScope = rememberCoroutineScope()
var canvasSize by remember { mutableStateOf(Size(100f, 100f)) }
val store = remember(canvasSize) {
DeviceDrawable2DStore(coroutineScope, canvasSize).apply {
coroutineScope.launch {
flowBuilder()
}
}
}
val drawables by store.drawableFlow.collectAsState()
key(store) {
Canvas(modifier.onGloballyPositioned {
canvasSize = it.size.toSize()
}) {
clipRect {
drawables.values.forEach {
with(it) { draw() }
}
onDraw()
}
}
}
}

@ -82,24 +82,22 @@ class DemoController : ContextAware {
opcUaServer.startup() opcUaServer.startup()
opcUaServer.serveDevices(deviceManager) opcUaServer.serveDevices(deviceManager)
//create a remote listener endpoint
val listenerEndpoint = MagixEndpoint.rSocketWithWebSockets("localhost") val listenerEndpoint = MagixEndpoint.rSocketWithWebSockets("localhost")
// subscribe remote endpoint
listenerEndpoint.subscribe(DeviceManager.magixFormat).onEach { (magixMessage, deviceMessage) -> listenerEndpoint.subscribe(DeviceManager.magixFormat).onEach { (magixMessage, deviceMessage) ->
// print all messages that are not property change message // print all messages that are not property change message
if (deviceMessage !is PropertyChangedMessage) { if (deviceMessage !is PropertyChangedMessage) {
println(">> ${json.encodeToString(MagixMessage.serializer(), magixMessage)}") println(">> ${json.encodeToString(MagixMessage.serializer(), magixMessage)}")
} }
}.launchIn(this) }.launchIn(this)
// send description request
listenerEndpoint.send( listenerEndpoint.send(
format = DeviceManager.magixFormat, format = DeviceManager.magixFormat,
payload = GetDescriptionMessage(), payload = GetDescriptionMessage(),
source = "listener", source = "listener",
// target = "demoDevice" // target = "demoDevice"
) )
} }
fun shutdown(): Job = context.launch { fun shutdown(): Job = context.launch {

@ -37,8 +37,8 @@ kotlin{
} }
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach { tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach {
compilerOptions { kotlinOptions {
freeCompilerArgs.addAll("-Xjvm-default=all") freeCompilerArgs = freeCompilerArgs + listOf("-Xjvm-default=all", "-Xopt-in=kotlin.RequiresOptIn")
} }
} }

@ -1,77 +0,0 @@
package space.kscience.controls.demo.constructor
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.ensureActive
import space.kscience.controls.constructor.DeviceConstructor
import space.kscience.controls.constructor.collectValuesIn
import space.kscience.controls.constructor.device
import space.kscience.controls.constructor.devices.LimitSwitch
import space.kscience.controls.constructor.devices.StepDrive
import space.kscience.controls.constructor.models.MutableRangeState
import space.kscience.controls.manager.DeviceManager
import space.kscience.dataforge.context.Context
import kotlin.time.Duration.Companion.seconds
private val ticksPerSecond = 3000.0
class LinearStepDrive(
context: Context,
drive: StepDrive,
atStart: LimitSwitch,
atEnd: LimitSwitch,
) : DeviceConstructor(context) {
val drive by device(drive)
val atStart by device(atStart)
val atEnd by device(atEnd)
}
fun LinearStepDrive(
context: Context,
position: MutableRangeState<Long>,
): LinearStepDrive = LinearStepDrive(
context = context,
drive = StepDrive(context, ticksPerSecond, position),
atStart = LimitSwitch(context, position.atStart),
atEnd = LimitSwitch(context, position.atEnd)
)
suspend fun LinearStepDrive.calibrate(step: Long = 10): ClosedRange<Long> = coroutineScope {
do {
ensureActive()
drive.target.value -= step
delay((step / ticksPerSecond).seconds)
} while (!atStart.locked.value)
val start = drive.position.value
do {
ensureActive()
drive.target.value += step
delay((step / ticksPerSecond).seconds)
} while (!atEnd.locked.value)
val end = drive.position.value
return@coroutineScope start..end
}
suspend fun main() = coroutineScope {
val context = Context {
plugin(DeviceManager)
}
val positionModel = MutableRangeState<Long>(0L, -1000L..1012L)
val linearStepDrive = LinearStepDrive(context, positionModel)
val printJob = linearStepDrive.drive.target.collectValuesIn(this){
println("Move to $it")
}
println(linearStepDrive.calibrate())
printJob.cancel()
}

@ -19,13 +19,10 @@ import io.github.koalaplot.core.util.toString
import io.github.koalaplot.core.xygraph.XYGraph import io.github.koalaplot.core.xygraph.XYGraph
import io.github.koalaplot.core.xygraph.rememberDoubleLinearAxisModel import io.github.koalaplot.core.xygraph.rememberDoubleLinearAxisModel
import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.datetime.Instant import kotlinx.datetime.Instant
import org.jetbrains.compose.splitpane.ExperimentalSplitPaneApi import org.jetbrains.compose.splitpane.ExperimentalSplitPaneApi
import org.jetbrains.compose.splitpane.HorizontalSplitPane import org.jetbrains.compose.splitpane.HorizontalSplitPane
import space.kscience.controls.api.PropertyChangedMessage
import space.kscience.controls.compose.NumberTextField import space.kscience.controls.compose.NumberTextField
import space.kscience.controls.compose.PlotNumericState import space.kscience.controls.compose.PlotNumericState
import space.kscience.controls.compose.TimeAxisModel import space.kscience.controls.compose.TimeAxisModel
@ -42,9 +39,11 @@ import space.kscience.controls.constructor.onTimer
import space.kscience.controls.constructor.units.Kilograms import space.kscience.controls.constructor.units.Kilograms
import space.kscience.controls.constructor.units.Meters import space.kscience.controls.constructor.units.Meters
import space.kscience.controls.constructor.units.NumericalValue import space.kscience.controls.constructor.units.NumericalValue
import space.kscience.controls.manager.* import space.kscience.controls.manager.ClockManager
import space.kscience.controls.manager.DeviceManager
import space.kscience.controls.manager.clock
import space.kscience.controls.manager.install
import space.kscience.dataforge.context.Context import space.kscience.dataforge.context.Context
import space.kscience.dataforge.context.request
import java.awt.Dimension import java.awt.Dimension
import kotlin.math.PI import kotlin.math.PI
import kotlin.math.sin import kotlin.math.sin
@ -166,15 +165,6 @@ fun main() = application {
//bind pid parameters //bind pid parameters
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
// start listening to local device hub
context.request(DeviceManager).hubMessageFlow()
.filterIsInstance<PropertyChangedMessage>() // filter only property change messages
//.filter { it.sourceDevice == "linearDrive".asName()} //optionally filter by device name
.onEach {
println("${it.sourceDevice} >> ${it.property} changed to ${it.value}")
}.launchIn(this)
snapshotFlow { snapshotFlow {
pidParameters pidParameters
}.onEach { }.onEach {

@ -1,29 +1,19 @@
package space.kscience.controls.demo.constructor package space.kscience.controls.demo.constructor
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.material.MaterialTheme
import androidx.compose.material3.Button
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Window import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application import androidx.compose.ui.window.application
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.isActive import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import org.jetbrains.compose.splitpane.ExperimentalSplitPaneApi
import org.jetbrains.compose.splitpane.HorizontalSplitPane
import space.kscience.controls.compose.*
import space.kscience.controls.constructor.* import space.kscience.controls.constructor.*
import space.kscience.controls.constructor.devices.LimitSwitch import space.kscience.controls.constructor.devices.LimitSwitch
import space.kscience.controls.constructor.devices.StepDrive import space.kscience.controls.constructor.devices.StepDrive
@ -102,7 +92,7 @@ private suspend fun Plotter.square(xRange: IntRange, yRange: IntRange) {
private val xRange = NumericalValue<Meters>(-0.5)..NumericalValue<Meters>(0.5) private val xRange = NumericalValue<Meters>(-0.5)..NumericalValue<Meters>(0.5)
private val yRange = NumericalValue<Meters>(-0.5)..NumericalValue<Meters>(0.5) private val yRange = NumericalValue<Meters>(-0.5)..NumericalValue<Meters>(0.5)
private const val ticksPerSecond = 3000.0 private val ticksPerSecond = MutableDeviceState(3000.0)
private val step = NumericalValue<Degrees>(1.8) private val step = NumericalValue<Degrees>(1.8)
@ -131,30 +121,24 @@ private class PlotterModel(
context = context, context = context,
xDrive = xDrive, xDrive = xDrive,
yDrive = yDrive, yDrive = yDrive,
xStartLimit = LimitSwitch(context, x.atStart), xStartLimit = LimitSwitch(context,x.atStart),
xEndLimit = LimitSwitch(context, x.atEnd), xEndLimit = LimitSwitch(context,x.atEnd),
yStartLimit = LimitSwitch(context, x.atStart), yStartLimit = LimitSwitch(context,x.atStart),
yEndLimit = LimitSwitch(context, x.atEnd), yEndLimit = LimitSwitch(context,x.atEnd),
) { color -> ) { color ->
println("Point X: ${x.value.value}, Y: ${y.value.value}, color: $color") println("Point X: ${x.value.value}, Y: ${y.value.value}, color: $color")
callback(PlotterPoint(x.value, y.value, color)) callback(PlotterPoint(x.value, y.value, color))
} }
} }
private val range = -1000..1000
@OptIn(ExperimentalSplitPaneApi::class)
suspend fun main() = application { suspend fun main() = application {
Window(title = "Pid regulator simulator", onCloseRequest = ::exitApplication) { Window(title = "Pid regulator simulator", onCloseRequest = ::exitApplication) {
window.minimumSize = Dimension(400, 400) window.minimumSize = Dimension(400, 400)
val scope = rememberCoroutineScope() val points = remember { mutableStateListOf<PlotterPoint>() }
var position by remember { mutableStateOf(XY<Meters>(0, 0)) }
var updateJob: Job? = remember { null } LaunchedEffect(Unit) {
var points by remember { mutableStateOf<List<PlotterPoint>>(emptyList()) }
val plotterModel = remember {
val context = Context { val context = Context {
plugin(DeviceManager) plugin(DeviceManager)
plugin(ClockManager) plugin(ClockManager)
@ -162,81 +146,53 @@ suspend fun main() = application {
/* Here goes the device definition block */ /* Here goes the device definition block */
PlotterModel(context) { plotterPoint -> val plotterModel = PlotterModel(context) { plotterPoint ->
points += plotterPoint points.add(plotterPoint)
} }
/* Start visualization program */
plotterModel.xy.valueFlow.onEach {
position = it
}.launchIn(this)
/* run program */
val range = -1000..1000
// plotterModel.plotter.modernArt(range, range)
plotterModel.plotter.square(range, range)
} }
/* Here goes the visualization block */ /* Here goes the visualization block */
MaterialTheme { MaterialTheme {
HorizontalSplitPane { Canvas(modifier = Modifier.fillMaxSize()) {
first(200.dp) { fun toOffset(x: NumericalValue<Meters>, y: NumericalValue<Meters>): Offset {
Column(modifier = Modifier.fillMaxHeight()) { val canvasX = (x - xRange.start) / (xRange.endInclusive - xRange.start) * size.width
Button({ val canvasY = (y - yRange.start) / (yRange.endInclusive - yRange.start) * size.height
updateJob?.cancel() return Offset(canvasX.toFloat(), canvasY.toFloat())
updateJob = scope.launch {
plotterModel.plotter.square(range, range)
}
}, modifier = Modifier.fillMaxWidth()) {
Text("Rectangle")
}
Button({
updateJob?.cancel()
updateJob = scope.launch {
plotterModel.plotter.modernArt(range, range)
}
}, modifier = Modifier.fillMaxWidth()) {
Text("Modern Art")
}
Button({
updateJob?.cancel()
}, modifier = Modifier.fillMaxWidth()) {
Text("Stop")
}
}
} }
second {
Device2DCanvas(modifier = Modifier.fillMaxSize()) {
fun xToPx(x: NumericalValue<Meters>): Float =
((x - xRange.start) / (xRange.endInclusive - xRange.start) * size.width).toFloat()
fun yToPx(y: NumericalValue<Meters>): Float = val center = toOffset(position.x, position.y)
((y - yRange.start) / (yRange.endInclusive - yRange.start) * size.height).toFloat()
fun toOffset(xy: XY<Meters>): Offset = Offset(xToPx(xy.x), yToPx(xy.y)) drawRect(
Color.LightGray,
topLeft = Offset(0f, center.y - 5f),
size = Size(size.width, 10f)
)
observeState(plotterModel.y, "beam") { y -> drawCircle(Color.Black, radius = 10f, center = center)
RectangleDrawable2D(
position = Offset(size.width / 2, yToPx(y)),
rectangleSize = Size(size.width, 10f),
color = Color.LightGray
)
}
observeState(plotterModel.xy, "head") { xy ->
CircleDrawable2D(
position = toOffset(xy),
radius = 10f,
color = Color.Black
)
}
snapshotFlow { points }.onEach { points.forEach {
it.forEachIndexed { index, plotterPoint -> drawCircle(it.color, radius = 2f, center = toOffset(it.x, it.y))
circle(
"point[$index]",
Offset(xToPx(plotterPoint.x), yToPx(plotterPoint.y)),
radius = 5f,
color = plotterPoint.color
)
}
}.launchIn(scope)
}
} }
} }
} }
} }
} }

@ -1,4 +1,14 @@
# Module device-collective # Module device-collective
# Running demo from gradle
1. Install JDK 17-21 in the system (for example, from https://sdkman.io/jdks or https://github.com/ScoopInstaller/Java).
2. Clone the repository with Git.
3. Run `./gradlew :demo:device-collective:run` from the project root directory.
# Install distribution
1. Install JDK 17-21 in the system (for example, from https://sdkman.io/jdks or https://github.com/ScoopInstaller/Java).
2. Clone the repository with Git.
3. Run `./gradlew :demo:device-collective:packageUberJarForCurrentOS` from the project root directory.
4. Go to `build/compose/jars/device-collective-<your OS>-<version>.jar`. You can copy it and run with `java -jar <full-name>.jar`

@ -21,8 +21,8 @@ kotlin{
} }
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach { tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach {
compilerOptions { kotlinOptions {
freeCompilerArgs.addAll("-Xjvm-default=all") freeCompilerArgs = freeCompilerArgs + listOf("-Xjvm-default=all", "-Xopt-in=kotlin.RequiresOptIn")
} }
} }

@ -26,8 +26,8 @@ kotlin{
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach { tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach {
compilerOptions { kotlinOptions {
freeCompilerArgs.addAll("-Xjvm-default=all") freeCompilerArgs = freeCompilerArgs + listOf("-Xjvm-default=all", "-Xopt-in=kotlin.RequiresOptIn")
} }
} }

@ -88,20 +88,20 @@ class MksPdr900Device(context: Context, meta: Meta) : DeviceBySpec<MksPdr900Devi
override fun build(context: Context, meta: Meta): MksPdr900Device = MksPdr900Device(context, meta) override fun build(context: Context, meta: Meta): MksPdr900Device = MksPdr900Device(context, meta)
val powerOn by mutableBooleanProperty(read = { readPowerOn() }, write = { _, value -> writePowerOn(value) }) val powerOn by booleanProperty(read = { readPowerOn() }, write = { _, value -> writePowerOn(value) })
val channel by property(MetaConverter.int) val channel by logicalProperty(MetaConverter.int)
val value by doubleProperty(read = { val value by doubleProperty(read = {
readChannelData(getOrRead(channel)) readChannelData(getOrRead(channel))
}) })
val error by property(MetaConverter.string) val error by logicalProperty(MetaConverter.string)
override suspend fun MksPdr900Device.onClose() { override fun MksPdr900Device.onClose() {
if (portDelegate.isInitialized()) { if (portDelegate.isInitialized()) {
port.stop() port.close()
} }
} }
} }

@ -23,6 +23,8 @@ import space.kscience.dataforge.context.*
import space.kscience.dataforge.meta.* import space.kscience.dataforge.meta.*
import space.kscience.dataforge.names.Name import space.kscience.dataforge.names.Name
import space.kscience.dataforge.names.parseAsName import space.kscience.dataforge.names.parseAsName
import kotlin.collections.component1
import kotlin.collections.component2
import kotlin.time.Duration import kotlin.time.Duration
import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.milliseconds
@ -166,7 +168,7 @@ class PiMotionMasterDevice(
} }
//Update port //Update port
//address = portSpec.node //address = portSpec.node
port = portFactory(portSpec, context).apply { start() } port = portFactory(portSpec, context).apply { open() }
// connector.open() // connector.open()
//Initialize axes //Initialize axes
val idn = read(identity) val idn = read(identity)
@ -188,7 +190,7 @@ class PiMotionMasterDevice(
}) { }) {
port?.let { port?.let {
execute(stop) execute(stop)
it.stop() it.close()
} }
port = null port = null
propertyChanged(connected, false) propertyChanged(connected, false)
@ -236,7 +238,7 @@ class PiMotionMasterDevice(
private fun axisBooleanProperty( private fun axisBooleanProperty(
command: String, command: String,
descriptorBuilder: PropertyDescriptor.() -> Unit = {}, descriptorBuilder: PropertyDescriptor.() -> Unit = {},
) = mutableBooleanProperty( ) = booleanProperty(
read = { read = {
readAxisBoolean("$command?") readAxisBoolean("$command?")
}, },
@ -249,7 +251,7 @@ class PiMotionMasterDevice(
private fun axisNumberProperty( private fun axisNumberProperty(
command: String, command: String,
descriptorBuilder: PropertyDescriptor.() -> Unit = {}, descriptorBuilder: PropertyDescriptor.() -> Unit = {},
) = mutableDoubleProperty( ) = doubleProperty(
read = { read = {
mm.requestAndParse("$command?", axisId)[axisId]?.toDoubleOrNull() mm.requestAndParse("$command?", axisId)[axisId]?.toDoubleOrNull()
?: error("Malformed $command response. Should include float value for $axisId") ?: error("Malformed $command response. Should include float value for $axisId")

@ -6,7 +6,6 @@ import kotlinx.coroutines.flow.*
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
import space.kscience.controls.api.AsynchronousSocket import space.kscience.controls.api.AsynchronousSocket
import space.kscience.controls.api.LifecycleState
import space.kscience.controls.ports.AbstractAsynchronousPort import space.kscience.controls.ports.AbstractAsynchronousPort
import space.kscience.controls.ports.withDelimiter import space.kscience.controls.ports.withDelimiter
import space.kscience.dataforge.context.* import space.kscience.dataforge.context.*
@ -49,10 +48,10 @@ abstract class VirtualDevice(val scope: CoroutineScope) : AsynchronousSocket<Byt
respond(response()) respond(response())
} }
override val lifecycleState: LifecycleState override val isOpen: Boolean
get() = if(scope.isActive) LifecycleState.STARTED else LifecycleState.STOPPED get() = scope.isActive
override suspend fun stop() = scope.cancel() override fun close() = scope.cancel()
} }
class VirtualPort(private val device: VirtualDevice, context: Context) : AbstractAsynchronousPort(context, Meta.EMPTY) { class VirtualPort(private val device: VirtualDevice, context: Context) : AbstractAsynchronousPort(context, Meta.EMPTY) {
@ -73,12 +72,12 @@ class VirtualPort(private val device: VirtualDevice, context: Context) : Abstrac
device.send(data) device.send(data)
} }
override val lifecycleState: LifecycleState override val isOpen: Boolean
get() = if(respondJob?.isActive == true) LifecycleState.STARTED else LifecycleState.STOPPED get() = respondJob?.isActive == true
override suspend fun stop() { override fun close() {
respondJob?.cancel() respondJob?.cancel()
super.stop() super.close()
} }
} }
@ -89,7 +88,7 @@ class PiMotionMasterVirtualDevice(
scope: CoroutineScope = context, scope: CoroutineScope = context,
) : VirtualDevice(scope), ContextAware { ) : VirtualDevice(scope), ContextAware {
override suspend fun start() { override fun open() {
//add asynchronous send logic here //add asynchronous send logic here
} }

@ -31,6 +31,8 @@ plc4j = "0.12.0"
visionforge = "0.4.2" visionforge = "0.4.2"
versions = "0.51.0"
[libraries] [libraries]
dataforge-io = { module = "space.kscience:dataforge-io", version.ref = "dataforge" } dataforge-io = { module = "space.kscience:dataforge-io", version.ref = "dataforge" }
@ -79,13 +81,10 @@ visionforge-markdown = { module = "space.kscience:visionforge-markdown", version
visionforge-server = { module = "space.kscience:visionforge-server", version.ref = "visionforge" } visionforge-server = { module = "space.kscience:visionforge-server", version.ref = "visionforge" }
visionforge-compose-html = { module = "space.kscience:visionforge-compose-html", version.ref = "visionforge" } visionforge-compose-html = { module = "space.kscience:visionforge-compose-html", version.ref = "visionforge" }
sciprog-maps-compose = { module = "space.kscience:maps-kt-compose", version = "0.3.0" } sciprog-maps-compose = "space.kscience:maps-kt-compose:0.3.0"
koala-plots = { module = "io.github.koalaplot:koalaplot-core", version = "0.6.1" }
# Buildscript # Buildscript
[plugins] [plugins]
versions = "com.github.ben-manes.versions:0.51.0" versions = { id = "com.github.ben-manes.versions", version.ref = "versions" }
versions-update = "nl.littlerobots.version-catalog-update:0.8.4"

@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists

@ -6,7 +6,7 @@ A kotlin API for magix standard and some zero-dependency magix services
## Artifact: ## Artifact:
The Maven coordinates of this project are `space.kscience:magix-api:0.4.0-dev-7`. The Maven coordinates of this project are `space.kscience:magix-api:0.4.0-dev-4`.
**Gradle Kotlin DSL:** **Gradle Kotlin DSL:**
```kotlin ```kotlin
@ -16,6 +16,6 @@ repositories {
} }
dependencies { dependencies {
implementation("space.kscience:magix-api:0.4.0-dev-7") implementation("space.kscience:magix-api:0.4.0-dev-4")
} }
``` ```

@ -13,7 +13,6 @@ kscience {
jvm() jvm()
js() js()
native() native()
wasm()
useCoroutines() useCoroutines()
useSerialization{ useSerialization{
json() json()

@ -6,7 +6,7 @@ Java API to work with magix endpoints without Kotlin
## Artifact: ## Artifact:
The Maven coordinates of this project are `space.kscience:magix-java-endpoint:0.4.0-dev-7`. The Maven coordinates of this project are `space.kscience:magix-java-endpoint:0.4.0-dev-4`.
**Gradle Kotlin DSL:** **Gradle Kotlin DSL:**
```kotlin ```kotlin
@ -16,6 +16,6 @@ repositories {
} }
dependencies { dependencies {
implementation("space.kscience:magix-java-endpoint:0.4.0-dev-7") implementation("space.kscience:magix-java-endpoint:0.4.0-dev-4")
} }
``` ```

@ -1,3 +1,5 @@
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
import space.kscience.gradle.KScienceVersions
import space.kscience.gradle.Maturity import space.kscience.gradle.Maturity
plugins { plugins {
@ -15,6 +17,19 @@ dependencies {
implementation(spclibs.kotlinx.coroutines.jdk9) implementation(spclibs.kotlinx.coroutines.jdk9)
} }
readme { //java {
// sourceCompatibility = KScienceVersions.JVM_TARGET
// targetCompatibility = KScienceVersions.JVM_TARGET
//}
//FIXME https://youtrack.jetbrains.com/issue/KT-52815/Compiler-option-Xjdk-release-fails-to-compile-mixed-projects
tasks.withType<KotlinCompile>{
kotlinOptions {
freeCompilerArgs -= "-Xjdk-release=11"
}
}
readme{
maturity = Maturity.EXPERIMENTAL maturity = Maturity.EXPERIMENTAL
} }

@ -6,7 +6,7 @@ MQTT client magix endpoint
## Artifact: ## Artifact:
The Maven coordinates of this project are `space.kscience:magix-mqtt:0.4.0-dev-7`. The Maven coordinates of this project are `space.kscience:magix-mqtt:0.4.0-dev-4`.
**Gradle Kotlin DSL:** **Gradle Kotlin DSL:**
```kotlin ```kotlin
@ -16,6 +16,6 @@ repositories {
} }
dependencies { dependencies {
implementation("space.kscience:magix-mqtt:0.4.0-dev-7") implementation("space.kscience:magix-mqtt:0.4.0-dev-4")
} }
``` ```

@ -1,5 +1,5 @@
plugins { plugins {
id("space.kscience.gradle.mpp") id("space.kscience.gradle.jvm")
`maven-publish` `maven-publish`
} }
@ -7,15 +7,12 @@ description = """
MQTT client magix endpoint MQTT client magix endpoint
""".trimIndent() """.trimIndent()
kscience { dependencies {
jvm() api(projects.magix.magixApi)
jvmMain { implementation(libs.hivemq.mqtt.client)
api(projects.magix.magixApi) implementation(spclibs.kotlinx.coroutines.jdk8)
implementation(libs.hivemq.mqtt.client)
implementation(spclibs.kotlinx.coroutines.jdk8)
}
} }
readme { readme{
maturity = space.kscience.gradle.Maturity.PROTOTYPE maturity = space.kscience.gradle.Maturity.PROTOTYPE
} }

@ -6,7 +6,7 @@ RabbitMQ client magix endpoint
## Artifact: ## Artifact:
The Maven coordinates of this project are `space.kscience:magix-rabbit:0.4.0-dev-7`. The Maven coordinates of this project are `space.kscience:magix-rabbit:0.4.0-dev-4`.
**Gradle Kotlin DSL:** **Gradle Kotlin DSL:**
```kotlin ```kotlin
@ -16,6 +16,6 @@ repositories {
} }
dependencies { dependencies {
implementation("space.kscience:magix-rabbit:0.4.0-dev-7") implementation("space.kscience:magix-rabbit:0.4.0-dev-4")
} }
``` ```

@ -6,7 +6,7 @@ Magix endpoint (client) based on RSocket
## Artifact: ## Artifact:
The Maven coordinates of this project are `space.kscience:magix-rsocket:0.4.0-dev-7`. The Maven coordinates of this project are `space.kscience:magix-rsocket:0.4.0-dev-4`.
**Gradle Kotlin DSL:** **Gradle Kotlin DSL:**
```kotlin ```kotlin
@ -16,6 +16,6 @@ repositories {
} }
dependencies { dependencies {
implementation("space.kscience:magix-rsocket:0.4.0-dev-7") implementation("space.kscience:magix-rsocket:0.4.0-dev-4")
} }
``` ```

@ -6,7 +6,7 @@ A magix event loop implementation in Kotlin. Includes HTTP/SSE and RSocket route
## Artifact: ## Artifact:
The Maven coordinates of this project are `space.kscience:magix-server:0.4.0-dev-7`. The Maven coordinates of this project are `space.kscience:magix-server:0.4.0-dev-4`.
**Gradle Kotlin DSL:** **Gradle Kotlin DSL:**
```kotlin ```kotlin
@ -16,6 +16,6 @@ repositories {
} }
dependencies { dependencies {
implementation("space.kscience:magix-server:0.4.0-dev-7") implementation("space.kscience:magix-server:0.4.0-dev-4")
} }
``` ```

@ -6,7 +6,7 @@ Magix history database API
## Artifact: ## Artifact:
The Maven coordinates of this project are `space.kscience:magix-storage:0.4.0-dev-7`. The Maven coordinates of this project are `space.kscience:magix-storage:0.4.0-dev-4`.
**Gradle Kotlin DSL:** **Gradle Kotlin DSL:**
```kotlin ```kotlin
@ -16,6 +16,6 @@ repositories {
} }
dependencies { dependencies {
implementation("space.kscience:magix-storage:0.4.0-dev-7") implementation("space.kscience:magix-storage:0.4.0-dev-4")
} }
``` ```

@ -6,7 +6,7 @@
## Artifact: ## Artifact:
The Maven coordinates of this project are `space.kscience:magix-storage-xodus:0.4.0-dev-7`. The Maven coordinates of this project are `space.kscience:magix-storage-xodus:0.4.0-dev-4`.
**Gradle Kotlin DSL:** **Gradle Kotlin DSL:**
```kotlin ```kotlin
@ -16,6 +16,6 @@ repositories {
} }
dependencies { dependencies {
implementation("space.kscience:magix-storage-xodus:0.4.0-dev-7") implementation("space.kscience:magix-storage-xodus:0.4.0-dev-4")
} }
``` ```

@ -1,23 +1,19 @@
plugins { plugins {
id("space.kscience.gradle.mpp") id("space.kscience.gradle.jvm")
`maven-publish` `maven-publish`
} }
kscience { kscience {
jvm()
useCoroutines() useCoroutines()
jvmMain {
api(projects.magix.magixStorage)
implementation(libs.xodus.entity.store)
// implementation("org.jetbrains.xodus:dnq:2.0.0")
}
jvmTest{
implementation(spclibs.kotlinx.coroutines.test)
}
} }
dependencies {
api(projects.magix.magixStorage)
implementation(libs.xodus.entity.store)
// implementation("org.jetbrains.xodus:dnq:2.0.0")
testImplementation(spclibs.kotlinx.coroutines.test)
}
readme { readme {
maturity = space.kscience.gradle.Maturity.PROTOTYPE maturity = space.kscience.gradle.Maturity.PROTOTYPE

@ -1,21 +0,0 @@
# Module magix-utils
Common utilities and services for Magix endpoints.
## Usage
## Artifact:
The Maven coordinates of this project are `space.kscience:magix-utils:0.4.0-dev-7`.
**Gradle Kotlin DSL:**
```kotlin
repositories {
maven("https://repo.kotlin.link")
mavenCentral()
}
dependencies {
implementation("space.kscience:magix-utils:0.4.0-dev-7")
}
```

@ -6,7 +6,7 @@ ZMQ client endpoint for Magix
## Artifact: ## Artifact:
The Maven coordinates of this project are `space.kscience:magix-zmq:0.4.0-dev-7`. The Maven coordinates of this project are `space.kscience:magix-zmq:0.4.0-dev-4`.
**Gradle Kotlin DSL:** **Gradle Kotlin DSL:**
```kotlin ```kotlin
@ -16,6 +16,6 @@ repositories {
} }
dependencies { dependencies {
implementation("space.kscience:magix-zmq:0.4.0-dev-7") implementation("space.kscience:magix-zmq:0.4.0-dev-4")
} }
``` ```

@ -1,7 +1,7 @@
import space.kscience.gradle.Maturity import space.kscience.gradle.Maturity
plugins { plugins {
id("space.kscience.gradle.mpp") id("space.kscience.gradle.jvm")
`maven-publish` `maven-publish`
} }
@ -9,13 +9,10 @@ description = """
ZMQ client endpoint for Magix ZMQ client endpoint for Magix
""".trimIndent() """.trimIndent()
kscience { dependencies {
jvm() api(projects.magix.magixApi)
jvmMain { api("org.slf4j:slf4j-api:2.0.6")
api(projects.magix.magixApi) api("org.zeromq:jeromq:0.5.3")
api("org.slf4j:slf4j-api:2.0.6")
api("org.zeromq:jeromq:0.5.3")
}
} }
readme { readme {

@ -4,6 +4,7 @@ import kotlinx.coroutines.*
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString import kotlinx.serialization.encodeToString
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import org.zeromq.SocketType import org.zeromq.SocketType

@ -21,10 +21,6 @@ pluginManagement {
} }
} }
plugins {
id("org.gradle.toolchains.foojay-resolver-convention") version "0.8.0"
}
dependencyResolutionManagement { dependencyResolutionManagement {
val toolsVersion: String by extra val toolsVersion: String by extra
@ -56,7 +52,6 @@ dependencyResolutionManagement {
} }
include( include(
":simulation-kt",
":controls-core", ":controls-core",
":controls-ports-ktor", ":controls-ports-ktor",
":controls-serial", ":controls-serial",

@ -1,26 +0,0 @@
# Module simulation-kt
## Features
- [timeline](#) : Timeline is an ordered discrete history containing TimeLineEvent
## Usage
## Artifact:
The Maven coordinates of this project are `space.kscience:simulation-kt:0.4.0-dev-7`.
**Gradle Kotlin DSL:**
```kotlin
repositories {
maven("https://repo.kotlin.link")
mavenCentral()
}
dependencies {
implementation("space.kscience:simulation-kt:0.4.0-dev-7")
}
```

@ -1,33 +0,0 @@
import space.kscience.gradle.Maturity
plugins {
id("space.kscience.gradle.mpp")
`maven-publish`
}
kscience {
jvm()
js()
native()
wasm()
useCoroutines()
useContextReceivers()
commonMain {
api(spclibs.kotlinx.datetime)
}
jvmTest {
implementation(spclibs.logback.classic)
}
}
readme {
maturity = Maturity.PROTOTYPE
description = """
A framework for combination of asynchronous simulations.
""".trimIndent()
feature("timeline") { "Timeline is an ordered discrete history containing TimeLineEvent" }
}

@ -1,66 +0,0 @@
package space.kscience.simulation
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.*
import kotlinx.datetime.Instant
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext
import kotlin.time.Duration
/**
* Suspend the collection of this [Flow] until event time is lower that threshold
*/
public fun <E : TimelineEvent> Flow<E>.withTimeThreshold(
threshold: Flow<Instant>
): Flow<E> = transform { event ->
threshold.first { it > event.time }
emit(event)
}
/**
* @param lookaheadInterval an interval for generated events ahead of the last observed event.
*/
public class GeneratingTimeline<E : TimelineEvent>(
origin: E,
private val lookaheadInterval: Duration,
coroutineContext: CoroutineContext = EmptyCoroutineContext,
private val generator: suspend FlowCollector<E>.(E) -> Unit
) : ProducerTimeline<E>(origin.time, coroutineContext) {
private val startEventFlow = MutableStateFlow(origin)
private data class EventWithOrigin<E : TimelineEvent>(val origin: E, val event: E) : TimelineEvent {
override val time: Instant get() = event.time
}
private val events: SharedFlow<E> = flow {
coroutineScope {
startEventFlow.collect { startEvent ->
emitAll(
flow { generator(startEvent) }.takeWhile { startEvent == startEventFlow.value }.map {
EventWithOrigin(startEvent, it)
}
)
}
}
}.withTimeThreshold(
threshold = time.map { it + lookaheadInterval }
).buffer(Channel.UNLIMITED).mapNotNull {
//a barrier to avoid leaking stale events after interruption from buffer
it.takeIf { it.origin == startEventFlow.value }?.event
}.shareIn(
scope = timelineScope,
started = SharingStarted.Lazily,
)
override fun events(): Flow<E> = events
public suspend fun interrupt(newStart: E) {
check(newStart.time >= time.value) {
"Can't interrupt generating timeline after observed event"
}
startTime = newStart.time
startEventFlow.emit(newStart)
}
}

@ -1,82 +0,0 @@
package space.kscience.simulation
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.datetime.Instant
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext
public class MergedTimeline<E : TimelineEvent>(
private val timelines: List<Timeline<E>>,
coroutineContext: CoroutineContext = EmptyCoroutineContext
) : Timeline<E> {
protected val timelineScope: CoroutineScope = CoroutineScope(
coroutineContext +
SupervisorJob(coroutineContext[Job]) +
CoroutineExceptionHandler{ _, throwable -> throwable.printStackTrace() } +
CoroutineName("MergedTimeline")
)
override val time: StateFlow<Instant> = combine(timelines.map { it.time }){ array->
array.max()
}.stateIn(timelineScope, SharingStarted.Lazily, timelines.maxOf { it.time.value })
override suspend fun advance(toTime: Instant) {
observers.forEach {
it.collect(toTime)
}
}
private val observers: MutableSet<TimelineObserver> = mutableSetOf()
override suspend fun observe(collector: suspend Flow<E>.() -> Unit): TimelineObserver {
val context = currentCoroutineContext()
val buffer = mutableListOf<E>()
val timelineObservers = timelines.map {
it.observeEach { event ->
buffer.add(event)
}
}
val observer = object : TimelineObserver {
private val channel = Channel<E>()
override val time = MutableStateFlow(this@MergedTimeline.time.value)
private val collectJob = timelineScope.launch(context) {
channel.consumeAsFlow().onEach {
time.emit(it.time)
}.collector()
}
private val mutex = Mutex()
override suspend fun collect(upTo: Instant) = mutex.withLock{
timelineObservers.forEach {
it.collect(upTo)
}
buffer.sortedBy { it.time }.forEach {
channel.send(it)
buffer.remove(it)
}
}
override fun close() {
collectJob.cancel()
observers.remove(this)
}
}
observers.add(observer)
return observer
}
}

@ -1,83 +0,0 @@
package space.kscience.simulation
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.datetime.Instant
import kotlin.coroutines.CoroutineContext
public abstract class ProducerTimeline<E : TimelineEvent>(
protected var startTime: Instant,
coroutineContext: CoroutineContext
) : Timeline<E>, AutoCloseable {
protected val timelineScope: CoroutineScope = CoroutineScope(
coroutineContext +
SupervisorJob(coroutineContext[Job]) +
CoroutineExceptionHandler{ _, throwable -> throwable.printStackTrace() } +
CoroutineName("Timeline")
)
private val observers: MutableSet<TimelineObserver> = mutableSetOf()
private val feedbackChannel = Channel<Unit>(onBufferOverflow = BufferOverflow.DROP_OLDEST)
override val time: StateFlow<Instant> = feedbackChannel.consumeAsFlow().map {
maxOf(startTime,observers.maxOfOrNull { it.time.value } ?: startTime)
}.stateIn(timelineScope, SharingStarted.Lazily, startTime)
override suspend fun advance(toTime: Instant) {
observers.forEach {
it.collect(toTime)
}
}
/**
* Flow unobserved events starting at [time]. The flow could be interrupted if timeline changes
*/
protected abstract fun events(): Flow<E>
override suspend fun observe(collector: suspend Flow<E>.() -> Unit): TimelineObserver {
val context = currentCoroutineContext()
val observer = object : TimelineObserver {
// observed time
override val time = MutableStateFlow(startTime)
private val channel = Channel<E>()
private val collectJob = timelineScope.launch(context) {
channel.consumeAsFlow().onEach {
time.emit(it.time)
feedbackChannel.send(Unit)
}.collector()
}
private val mutex = Mutex()
override suspend fun collect(upTo: Instant) = mutex.withLock {
require(upTo >= time.value) { "Requested time $upTo is lower than observed ${time.value}" }
events().takeWhile {
it.time <= upTo
}.collect {
channel.send(it)
}
}
override fun close() {
collectJob.cancel()
observers.remove(this)
}
}
observers.add(observer)
return observer
}
override fun close() {
observers.forEach { it.close() }
timelineScope.cancel()
}
}

@ -1,31 +0,0 @@
package space.kscience.simulation
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.datetime.Instant
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext
/**
* A manually mutable [Timeline] that could be modified via [emit] method by multiple
*/
public class SharedTimeline<E : TimelineEvent>(
startTime: Instant,
coroutineContext: CoroutineContext = EmptyCoroutineContext
) : ProducerTimeline<E>(startTime, coroutineContext) {
private val events = MutableSharedFlow<E>(replay = Channel.UNLIMITED)
override fun events(): Flow<E> = events
/**
* Emit new event to the timeline
*/
public suspend fun emit(event: E) {
if (event.time < (events.replayCache.lastOrNull()?.time ?: time.value)) {
error("Can't emit event $event because timeline monotony is broken")
}
events.emit(event)
}
}

@ -1,81 +0,0 @@
package space.kscience.simulation
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.datetime.Instant
import kotlin.time.Duration
public interface TimelineEvent {
public val time: Instant
}
public interface TimelineInterval : TimelineEvent {
public val startTime: Instant
public val duration: Duration
override val time: Instant
get() = startTime + duration
}
public data class SimpleTimelineEvent<T>(override val time: Instant, val value: T) : TimelineEvent
public interface TimelineObserver : AutoCloseable {
/**
* The subjective time of this observer (last observed time)
*/
public val time: StateFlow<Instant>
/**
* Collect all uncollected events from [time] to [upTo].
*
* By default, collects all events.
*/
public suspend fun collect(upTo: Instant = Instant.DISTANT_FUTURE)
}
/**
* Collect events for a fixed [duration] since last observed time
*/
public suspend fun TimelineObserver.collect(duration: Duration): Unit = collect(time.value + duration)
/**
* A time-ordered sequence of events of type [E]. There time of events is strictly monotonic, meaning that the time of
* the next event is greater than the previous event time.
*
* Timeline guarantees that all collectors could read all events when they need. Meaning that all unread events are cached.
*
* Timeline guarantees that already read events won't change, but unread events could change.
*/
public interface Timeline<E : TimelineEvent> {
/**
* A subjective time of this timeline. The subjective time is the last observed time.
*/
public val time: StateFlow<Instant>
/**
* Attach observer to this [Timeline]. The observer collection is not triggered right away, but only on demand.
*
* Each collection shifts [TimelineObserver.time] for this observer.
*/
public suspend fun observe(
collector: suspend Flow<E>.() -> Unit
): TimelineObserver
/**
* Advance simulation time to [toTime]. This method forces all observers to collect all events in the given range.
*
* This method suspends until all advancement is done
*/
public suspend fun advance(toTime: Instant)
}
/**
* Perform [collector] action on each event
*/
public suspend fun <E : TimelineEvent> Timeline<E>.observeEach(
collector: suspend (E) -> Unit
): TimelineObserver = observe {
collect(collector)
}

@ -1,35 +0,0 @@
package space.kscience.simulation
internal inline fun <T, R : Comparable<R>> Iterable<T>.minOfNotNullOrNull(selector: (T) -> R?): R? {
val iterator = iterator()
if (!iterator.hasNext()) return null
var minValue = selector(iterator.next())
while (iterator.hasNext()) {
val v = selector(iterator.next())
when {
minValue == null -> minValue = v
v == null -> {/*do nothing*/}
minValue > v -> {
minValue = v
}
}
}
return minValue
}
internal inline fun <T, R : Comparable<R>> Iterable<T>.maxOfNotNullOrNull(selector: (T) -> R?): R? {
val iterator = iterator()
if (!iterator.hasNext()) return null
var maxValue = selector(iterator.next())
while (iterator.hasNext()) {
val v = selector(iterator.next())
when {
maxValue == null -> maxValue = v
v == null -> {/*do nothing*/}
maxValue < v -> {
maxValue = v
}
}
}
return maxValue
}

@ -1,48 +0,0 @@
package space.kscience.simulation
import kotlinx.coroutines.isActive
import kotlinx.coroutines.test.runTest
import kotlinx.datetime.Instant
import kotlin.test.Test
import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds
class TimelineTests {
@Test
fun testGeneration() = runTest(timeout = 1.seconds) {
val startTime = Instant.parse("2020-01-01T00:00:00.000Z")
val generation = GeneratingTimeline(
origin = SimpleTimelineEvent(startTime, Unit),
lookaheadInterval = 1.seconds
) { event ->
var time = event.time
while (isActive) {
time += 0.1.seconds
println("Emit: ${time - startTime}")
emit(SimpleTimelineEvent(time, Unit))
}
}
val result = mutableListOf<Duration>()
val observer = generation.observeEach {
println("Consume: ${it.time - startTime}")
result.add(it.time - startTime)
}
observer.collect(2.seconds)
println("First collection complete")
observer.collect(2.seconds)
println("Second collection complete")
println("Interrupt")
generation.interrupt(SimpleTimelineEvent(startTime + 6.seconds, Unit))
println("Collecting after interruption")
observer.collect(startTime + 6.seconds + 2.5.seconds)
println(result)
generation.close()
}
}