Compare commits
No commits in common. "master" and "feature/device-collective-demo" have entirely different histories.
master
...
feature/de
.space.ktsCHANGELOG.mdREADME.mdbuild.gradle.ktssettings.gradle.kts
controls-constructor
README.mdbuild.gradle.kts
src
commonMain/kotlin/space/kscience/controls/constructor
commonTest/kotlin/space/kscience/controls/constructor
controls-core
README.mdbuild.gradle.kts
src
commonMain/kotlin/space/kscience/controls
api
ports
spec
jvmMain/kotlin/space/kscience/controls/ports
jvmTest/kotlin/space/kscience/controls/ports
wasmJsMain/kotlin
controls-jupyter
controls-magix
controls-modbus
controls-opcua
controls-pi
controls-plc4x
controls-ports-ktor
controls-serial
README.md
src/jvmMain/kotlin/space/kscience/controls/serial
controls-server
controls-storage
controls-vision
controls-visualisation-compose
demo
all-things/src/main/kotlin/space/kscience/controls/demo
car
build.gradle.kts
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
magix-rsocket
magix-server
magix-storage
magix-utils
magix-zmq
simulation-kt
README.mdbuild.gradle.kts
src
commonMain/kotlin
GeneratingTimeline.ktMergedTimeline.ktProducerTimeline.ktSharedTimeline.ktTimeline.ktnotNullUtils.kt
commonTest/kotlin
45
.space.kts
Normal file
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,
|
||||||
|
23
controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/devices/StepDrive.kt
23
controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/devices/StepDrive.kt
@ -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"
|
|
||||||
|
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@ -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()
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
x
Reference in New Issue
Block a user