[WIP] Refactor constructor

This commit is contained in:
Alexander Nozik 2024-05-15 22:49:08 +03:00
parent a9592d0372
commit 5921978122
44 changed files with 367 additions and 229 deletions

View File

@ -104,6 +104,11 @@ Automatically checks consistency.
> >
> **Maturity**: EXPERIMENTAL > **Maturity**: EXPERIMENTAL
### [controls-plc4x](controls-plc4x)
> A plugin for Controls-kt device server on top of plc4x library
>
> **Maturity**: EXPERIMENTAL
### [controls-ports-ktor](controls-ports-ktor) ### [controls-ports-ktor](controls-ports-ktor)
> Implementation of byte ports on top os ktor-io asynchronous API > Implementation of byte ports on top os ktor-io asynchronous API
> >
@ -209,6 +214,11 @@ Automatically checks consistency.
> >
> **Maturity**: PROTOTYPE > **Maturity**: PROTOTYPE
### [magix/magix-utils](magix/magix-utils)
> Common utilities and services for Magix endpoints.
>
> **Maturity**: EXPERIMENTAL
### [magix/magix-zmq](magix/magix-zmq) ### [magix/magix-zmq](magix/magix-zmq)
> ZMQ client endpoint for Magix > ZMQ client endpoint for Magix
> >

View File

@ -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.3.0`. The Maven coordinates of this project are `space.kscience:controls-constructor:0.4.0-dev-1`.
**Gradle Kotlin DSL:** **Gradle Kotlin DSL:**
```kotlin ```kotlin
@ -16,6 +16,6 @@ repositories {
} }
dependencies { dependencies {
implementation("space.kscience:controls-constructor:0.3.0") implementation("space.kscience:controls-constructor:0.4.0-dev-1")
} }
``` ```

View File

@ -2,7 +2,10 @@ package space.kscience.controls.constructor
import space.kscience.controls.api.Device import space.kscience.controls.api.Device
public sealed interface ConstructorBinding /**
* A binding that is used to describe device functionality
*/
public sealed interface Binding
/** /**
* A binding that exposes device property as read-only state * A binding that exposes device property as read-only state
@ -11,16 +14,22 @@ public class PropertyBinding<T>(
public val device: Device, public val device: Device,
public val propertyName: String, public val propertyName: String,
public val state: DeviceState<T>, public val state: DeviceState<T>,
) : ConstructorBinding ) : Binding
/** /**
* A binding for independent state like a timer * A binding for independent state like a timer
*/ */
public class StateBinding<T>( public class StateBinding<T>(
public val state: DeviceState<T> public val state: DeviceState<T>
) : ConstructorBinding ) : Binding
public class ActionBinding( public class ActionBinding(
public val reads: Collection<DeviceState<*>>, public val reads: Collection<DeviceState<*>>,
public val writes: Collection<DeviceState<*>> public val writes: Collection<DeviceState<*>>
): ConstructorBinding ): Binding
public interface BindingsContainer{
public val bindings: List<Binding>
public fun registerBinding(binding: Binding)
}

View File

@ -26,11 +26,11 @@ import kotlin.time.Duration
public abstract class DeviceConstructor( public abstract class DeviceConstructor(
context: Context, context: Context,
meta: Meta = Meta.EMPTY, meta: Meta = Meta.EMPTY,
) : DeviceGroup(context, meta) { ) : DeviceGroup(context, meta), BindingsContainer {
private val _bindings: MutableList<ConstructorBinding> = mutableListOf() private val _bindings: MutableList<Binding> = mutableListOf()
public val bindings: List<ConstructorBinding> get() = _bindings override val bindings: List<Binding> get() = _bindings
public fun registerBinding(binding: ConstructorBinding) { override fun registerBinding(binding: Binding) {
_bindings.add(binding) _bindings.add(binding)
} }
@ -46,7 +46,7 @@ public abstract class DeviceConstructor(
.also { registerBinding(StateBinding(it)) } .also { registerBinding(StateBinding(it)) }
/** /**
* Launch action that is performed on each [DeviceState] value change. * Bind an action to a [DeviceState]. [onChange] block is performed on each state change
* *
* Optionally provide [writes] - a set of states that this change affects. * Optionally provide [writes] - a set of states that this change affects.
*/ */

View File

@ -0,0 +1,12 @@
package space.kscience.controls.constructor
public abstract class DeviceModel : BindingsContainer {
private val _bindings: MutableList<Binding> = mutableListOf()
override val bindings: List<Binding> get() = _bindings
override fun registerBinding(binding: Binding) {
_bindings.add(binding)
}
}

View File

@ -63,25 +63,25 @@ public interface DeviceStateWithDependencies<T> : DeviceState<T> {
/** /**
* Create a new read-only [DeviceState] that mirrors receiver state by mapping the value with [mapper]. * Create a new read-only [DeviceState] that mirrors receiver state by mapping the value with [mapper].
*/ */
public fun <T, R> DeviceState<T>.map( public fun <T, R> DeviceState.Companion.map(
state: DeviceState<T>,
converter: MetaConverter<R>, mapper: (T) -> R, converter: MetaConverter<R>, mapper: (T) -> R,
): DeviceStateWithDependencies<R> = object : DeviceStateWithDependencies<R> { ): DeviceStateWithDependencies<R> = object : DeviceStateWithDependencies<R> {
override val dependencies = listOf(this) override val dependencies = listOf(state)
override val converter: MetaConverter<R> = converter override val converter: MetaConverter<R> = converter
override val value: R override val value: R get() = mapper(state.value)
get() = mapper(this@map.value)
override val valueFlow: Flow<R> = this@map.valueFlow.map(mapper) override val valueFlow: Flow<R> = state.valueFlow.map(mapper)
override fun toString(): String = "DeviceState.map(arg=${this@map}, converter=$converter)" override fun toString(): String = "DeviceState.map(arg=${state}, converter=$converter)"
} }
/** /**
* Combine two device states into one read-only [DeviceState]. Only the latest value of each state is used. * Combine two device states into one read-only [DeviceState]. Only the latest value of each state is used.
*/ */
public fun <T1, T2, R> combine( public fun <T1, T2, R> DeviceState.Companion.combine(
state1: DeviceState<T1>, state1: DeviceState<T1>,
state2: DeviceState<T2>, state2: DeviceState<T2>,
converter: MetaConverter<R>, converter: MetaConverter<R>,

View File

@ -1,5 +1,6 @@
package space.kscience.controls.constructor package space.kscience.controls.constructor
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.isActive import kotlinx.coroutines.isActive
@ -26,9 +27,9 @@ public class TimerState(
private val clock = MutableStateFlow(clockManager.clock.now()) private val clock = MutableStateFlow(clockManager.clock.now())
private val updateJob = clockManager.context.launch { private val updateJob = clockManager.context.launch(clockManager.asDispatcher()) {
while (isActive) { while (isActive) {
clockManager.delay(tick) delay(tick)
clock.value = clockManager.clock.now() clock.value = clockManager.clock.now()
} }
} }

View File

@ -8,7 +8,7 @@ import space.kscience.dataforge.meta.MetaConverter
/** /**
* A state describing a [Double] value in the [range] * A state describing a [Double] value in the [range]
*/ */
public class DoubleRangeState( public class DoubleInRangeState(
initialValue: Double, initialValue: Double,
public val range: ClosedFloatingPointRange<Double>, public val range: ClosedFloatingPointRange<Double>,
) : MutableDeviceState<Double> { ) : MutableDeviceState<Double> {
@ -32,20 +32,28 @@ public class DoubleRangeState(
/** /**
* A state showing that the range is on its lower boundary * A state showing that the range is on its lower boundary
*/ */
public val atStartState: DeviceState<Boolean> = map(MetaConverter.boolean) { it <= range.start } public val atStart: DeviceState<Boolean> = DeviceState.map(this, MetaConverter.boolean) {
it <= range.start
}
/** /**
* A state showing that the range is on its higher boundary * A state showing that the range is on its higher boundary
*/ */
public val atEndState: DeviceState<Boolean> = map(MetaConverter.boolean) { it >= range.endInclusive } public val atEnd: DeviceState<Boolean> = DeviceState.map(this, MetaConverter.boolean) {
it >= range.endInclusive
}
override fun toString(): String = "DoubleRangeState(range=$range, converter=$converter)" override fun toString(): String = "DoubleRangeState(range=$range, converter=$converter)"
} }
@Suppress("UnusedReceiverParameter") /**
public fun DeviceGroup.rangeState( * Create and register a [DoubleInRangeState]
*/
public fun BindingsContainer.doubleInRangeState(
initialValue: Double, initialValue: Double,
range: ClosedFloatingPointRange<Double>, range: ClosedFloatingPointRange<Double>,
): DoubleRangeState = DoubleRangeState(initialValue, range) ): DoubleInRangeState = DoubleInRangeState(initialValue, range).also {
registerBinding(StateBinding(it))
}

View File

@ -25,8 +25,6 @@ private class VirtualDeviceState<T>(
} }
override fun toString(): String = "VirtualDeviceState(converter=$converter)" override fun toString(): String = "VirtualDeviceState(converter=$converter)"
} }

View File

@ -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.3.0`. The Maven coordinates of this project are `space.kscience:controls-core:0.4.0-dev-1`.
**Gradle Kotlin DSL:** **Gradle Kotlin DSL:**
```kotlin ```kotlin
@ -26,6 +26,6 @@ repositories {
} }
dependencies { dependencies {
implementation("space.kscience:controls-core:0.3.0") implementation("space.kscience:controls-core:0.4.0-dev-1")
} }
``` ```

View File

@ -1,24 +1,80 @@
package space.kscience.controls.manager package space.kscience.controls.manager
import kotlinx.coroutines.*
import kotlinx.datetime.Clock import kotlinx.datetime.Clock
import space.kscience.dataforge.context.AbstractPlugin import kotlinx.datetime.Instant
import space.kscience.dataforge.context.Context import space.kscience.controls.api.Device
import space.kscience.dataforge.context.PluginFactory import space.kscience.dataforge.context.*
import space.kscience.dataforge.context.PluginTag
import space.kscience.dataforge.meta.Meta import space.kscience.dataforge.meta.Meta
import kotlin.time.Duration import space.kscience.dataforge.meta.double
import kotlin.coroutines.CoroutineContext
import kotlin.math.roundToLong
@OptIn(InternalCoroutinesApi::class)
private class CompressedTimeDispatcher(
val dispatcher: CoroutineDispatcher,
val compression: Double,
) : CoroutineDispatcher(), Delay {
@InternalCoroutinesApi
override fun dispatchYield(context: CoroutineContext, block: Runnable) {
dispatcher.dispatchYield(context, block)
}
override fun isDispatchNeeded(context: CoroutineContext): Boolean = dispatcher.isDispatchNeeded(context)
@ExperimentalCoroutinesApi
override fun limitedParallelism(parallelism: Int): CoroutineDispatcher = dispatcher.limitedParallelism(parallelism)
override fun dispatch(context: CoroutineContext, block: Runnable) {
dispatcher.dispatch(context, block)
}
private val delay = ((dispatcher as? Delay) ?: (Dispatchers.Default as Delay))
override fun scheduleResumeAfterDelay(timeMillis: Long, continuation: CancellableContinuation<Unit>) {
delay.scheduleResumeAfterDelay((timeMillis / compression).roundToLong(), continuation)
}
override fun invokeOnTimeout(timeMillis: Long, block: Runnable, context: CoroutineContext): DisposableHandle {
return delay.invokeOnTimeout((timeMillis / compression).roundToLong(), block, context)
}
}
private class CompressedClock(
val start: Instant,
val compression: Double,
val baseClock: Clock = Clock.System,
) : Clock {
override fun now(): Instant {
val elapsed = (baseClock.now() - start)
return start + elapsed / compression
}
}
public class ClockManager : AbstractPlugin() { public class ClockManager : AbstractPlugin() {
override val tag: PluginTag get() = Companion.tag override val tag: PluginTag get() = Companion.tag
public val timeCompression: Double by meta.double(1.0)
public val clock: Clock by lazy { public val clock: Clock by lazy {
//TODO add clock customization if (timeCompression == 1.0) {
Clock.System Clock.System
} else {
CompressedClock(Clock.System.now(), timeCompression)
}
} }
public suspend fun delay(duration: Duration) { /**
//TODO add time compression * Provide a [CoroutineDispatcher] with compressed time based on given [dispatcher]
kotlinx.coroutines.delay(duration) */
public fun asDispatcher(
dispatcher: CoroutineDispatcher = Dispatchers.Default,
): CoroutineDispatcher = if (timeCompression == 1.0) {
dispatcher
} else {
CompressedTimeDispatcher(dispatcher, timeCompression)
} }
public companion object : PluginFactory<ClockManager> { public companion object : PluginFactory<ClockManager> {
@ -29,3 +85,15 @@ public class ClockManager : AbstractPlugin() {
} }
public val Context.clock: Clock get() = plugins[ClockManager]?.clock ?: Clock.System public val Context.clock: Clock get() = plugins[ClockManager]?.clock ?: Clock.System
public val Device.clock: Clock get() = context.clock
public fun Device.getCoroutineDispatcher(dispatcher: CoroutineDispatcher = Dispatchers.Default): CoroutineDispatcher =
context.plugins[ClockManager]?.asDispatcher(dispatcher) ?: dispatcher
public fun ContextBuilder.withTimeCompression(compression: Double) {
require(compression > 0.0) { "Time compression must be greater than zero." }
plugin(ClockManager) {
"timeCompression" put compression
}
}

View File

@ -4,6 +4,7 @@ import kotlinx.coroutines.*
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flow
import space.kscience.controls.api.Device import space.kscience.controls.api.Device
import space.kscience.controls.manager.getCoroutineDispatcher
import kotlin.time.Duration import kotlin.time.Duration
/** /**
@ -15,11 +16,12 @@ public fun <D : Device> D.doRecurring(
task: suspend D.() -> Unit, task: suspend D.() -> Unit,
): Job { ): Job {
val taskName = debugTaskName ?: "task[${task.hashCode().toString(16)}]" val taskName = debugTaskName ?: "task[${task.hashCode().toString(16)}]"
return launch(CoroutineName(taskName)) { val dispatcher = getCoroutineDispatcher()
return launch(CoroutineName(taskName) + dispatcher) {
while (isActive) { while (isActive) {
delay(interval) delay(interval)
//launch in parent scope to properly evaluate exceptions //launch in parent scope to properly evaluate exceptions
this@doRecurring.launch { this@doRecurring.launch(CoroutineName("$taskName-recurring") + dispatcher) {
task() task()
} }
} }

View File

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

View File

@ -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.3.0`. The Maven coordinates of this project are `space.kscience:controls-magix:0.4.0-dev-1`.
**Gradle Kotlin DSL:** **Gradle Kotlin DSL:**
```kotlin ```kotlin
@ -22,6 +22,6 @@ repositories {
} }
dependencies { dependencies {
implementation("space.kscience:controls-magix:0.3.0") implementation("space.kscience:controls-magix:0.4.0-dev-1")
} }
``` ```

View File

@ -12,7 +12,8 @@ description = """
kscience { kscience {
jvm() jvm()
js() js()
useCoroutines("1.8.0") native()
useCoroutines()
useSerialization { useSerialization {
json() json()
} }

View File

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

View File

@ -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.3.0`. The Maven coordinates of this project are `space.kscience:controls-opcua:0.4.0-dev-1`.
**Gradle Kotlin DSL:** **Gradle Kotlin DSL:**
```kotlin ```kotlin
@ -22,6 +22,6 @@ repositories {
} }
dependencies { dependencies {
implementation("space.kscience:controls-opcua:0.3.0") implementation("space.kscience:controls-opcua:0.4.0-dev-1")
} }
``` ```

View File

@ -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.3.0`. The Maven coordinates of this project are `space.kscience:controls-pi:0.4.0-dev-1`.
**Gradle Kotlin DSL:** **Gradle Kotlin DSL:**
```kotlin ```kotlin
@ -16,6 +16,6 @@ repositories {
} }
dependencies { dependencies {
implementation("space.kscience:controls-pi:0.3.0") implementation("space.kscience:controls-pi:0.4.0-dev-1")
} }
``` ```

View File

@ -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.3.0`. The Maven coordinates of this project are `space.kscience:controls-ports-ktor:0.4.0-dev-1`.
**Gradle Kotlin DSL:** **Gradle Kotlin DSL:**
```kotlin ```kotlin
@ -16,6 +16,6 @@ repositories {
} }
dependencies { dependencies {
implementation("space.kscience:controls-ports-ktor:0.3.0") implementation("space.kscience:controls-ports-ktor:0.4.0-dev-1")
} }
``` ```

View File

@ -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.3.0`. The Maven coordinates of this project are `space.kscience:controls-serial:0.4.0-dev-1`.
**Gradle Kotlin DSL:** **Gradle Kotlin DSL:**
```kotlin ```kotlin
@ -16,6 +16,6 @@ repositories {
} }
dependencies { dependencies {
implementation("space.kscience:controls-serial:0.3.0") implementation("space.kscience:controls-serial:0.4.0-dev-1")
} }
``` ```

View File

@ -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.3.0`. The Maven coordinates of this project are `space.kscience:controls-server:0.4.0-dev-1`.
**Gradle Kotlin DSL:** **Gradle Kotlin DSL:**
```kotlin ```kotlin
@ -16,6 +16,6 @@ repositories {
} }
dependencies { dependencies {
implementation("space.kscience:controls-server:0.3.0") implementation("space.kscience:controls-server:0.4.0-dev-1")
} }
``` ```

View File

@ -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.3.0`. The Maven coordinates of this project are `space.kscience:controls-storage:0.4.0-dev-1`.
**Gradle Kotlin DSL:** **Gradle Kotlin DSL:**
```kotlin ```kotlin
@ -16,6 +16,6 @@ repositories {
} }
dependencies { dependencies {
implementation("space.kscience:controls-storage:0.3.0") implementation("space.kscience:controls-storage:0.4.0-dev-1")
} }
``` ```

View File

@ -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.3.0`. The Maven coordinates of this project are `space.kscience:controls-xodus:0.4.0-dev-1`.
**Gradle Kotlin DSL:** **Gradle Kotlin DSL:**
```kotlin ```kotlin
@ -16,6 +16,6 @@ repositories {
} }
dependencies { dependencies {
implementation("space.kscience:controls-xodus:0.3.0") implementation("space.kscience:controls-xodus:0.4.0-dev-1")
} }
``` ```

View File

@ -2,11 +2,13 @@
Dashboard and visualization extensions for devices Dashboard and visualization extensions for devices
Hello world!
## Usage ## Usage
## Artifact: ## Artifact:
The Maven coordinates of this project are `space.kscience:controls-vision:0.3.0`. The Maven coordinates of this project are `space.kscience:controls-vision:0.4.0-dev-1`.
**Gradle Kotlin DSL:** **Gradle Kotlin DSL:**
```kotlin ```kotlin
@ -16,6 +18,6 @@ repositories {
} }
dependencies { dependencies {
implementation("space.kscience:controls-vision:0.3.0") implementation("space.kscience:controls-vision:0.4.0-dev-1")
} }
``` ```

View File

@ -10,6 +10,7 @@ description = """
kscience { kscience {
fullStack("js/controls-vision.js") fullStack("js/controls-vision.js")
useKtor() useKtor()
useSerialization()
useContextReceivers() useContextReceivers()
dependencies { dependencies {
api(projects.controlsCore) api(projects.controlsCore)

View File

@ -0,0 +1,15 @@
# Module ${name}
${description}
<#if features?has_content>
## Features
${features}
</#if>
<#if published>
## Usage
${artifact}
</#if>

View File

@ -0,0 +1,24 @@
package space.kscience.controls.vision
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import space.kscience.dataforge.meta.boolean
import space.kscience.visionforge.AbstractVision
import space.kscience.visionforge.Vision
import space.kscience.visionforge.html.VisionOfHtml
/**
* A [Vision] that shows an indicator
*/
@Serializable
@SerialName("controls.indicator")
public class BooleanIndicatorVision : AbstractVision(), VisionOfHtml {
public val isOn: Boolean by properties.boolean(false)
}
///**
// * A [Vision] that allows both showing the value and changing it
// */
//public interface RegulatorVision: IndicatorVision{
//
//}

View File

@ -6,7 +6,6 @@ import kotlinx.serialization.modules.subclass
import space.kscience.dataforge.context.PluginFactory import space.kscience.dataforge.context.PluginFactory
import space.kscience.visionforge.Vision import space.kscience.visionforge.Vision
import space.kscience.visionforge.VisionPlugin import space.kscience.visionforge.VisionPlugin
import space.kscience.visionforge.plotly.VisionOfPlotly
public expect class ControlVisionPlugin: VisionPlugin{ public expect class ControlVisionPlugin: VisionPlugin{
public companion object: PluginFactory<ControlVisionPlugin> public companion object: PluginFactory<ControlVisionPlugin>
@ -14,6 +13,6 @@ public expect class ControlVisionPlugin: VisionPlugin{
internal val controlsVisionSerializersModule = SerializersModule { internal val controlsVisionSerializersModule = SerializersModule {
polymorphic(Vision::class) { polymorphic(Vision::class) {
subclass(VisionOfPlotly.serializer()) subclass(BooleanIndicatorVision.serializer())
} }
} }

View File

@ -1,20 +0,0 @@
package space.kscience.controls.vision
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.node
import space.kscience.visionforge.AbstractVision
import space.kscience.visionforge.Vision
/**
* A [Vision] that shows an indicator
*/
public class IndicatorVision: AbstractVision() {
public val value: Meta? by properties.node()
}
///**
// * A [Vision] that allows both showing the value and changing it
// */
//public interface RegulatorVision: IndicatorVision{
//
//}

View File

@ -1,4 +1,4 @@
@file:OptIn(FlowPreview::class) @file:OptIn(FlowPreview::class, FlowPreview::class)
package space.kscience.controls.vision package space.kscience.controls.vision

View File

@ -5,13 +5,29 @@ import space.kscience.dataforge.context.Context
import space.kscience.dataforge.context.PluginFactory import space.kscience.dataforge.context.PluginFactory
import space.kscience.dataforge.context.PluginTag import space.kscience.dataforge.context.PluginTag
import space.kscience.dataforge.meta.Meta import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.names.Name
import space.kscience.dataforge.names.asName
import space.kscience.visionforge.VisionPlugin import space.kscience.visionforge.VisionPlugin
import space.kscience.visionforge.html.ElementVisionRenderer
private val indicatorRenderer = ElementVisionRenderer<BooleanIndicatorVision> { name, vision: BooleanIndicatorVision, meta ->
}
public actual class ControlVisionPlugin : VisionPlugin() { public actual class ControlVisionPlugin : VisionPlugin() {
override val tag: PluginTag get() = Companion.tag override val tag: PluginTag get() = Companion.tag
override val visionSerializersModule: SerializersModule get() = controlsVisionSerializersModule override val visionSerializersModule: SerializersModule get() = controlsVisionSerializersModule
override fun content(target: String): Map<Name, Any> = when (target) {
ElementVisionRenderer.TYPE -> mapOf(
"indicator".asName() to indicatorRenderer
)
else -> super.content(target)
}
public actual companion object : PluginFactory<ControlVisionPlugin> { public actual companion object : PluginFactory<ControlVisionPlugin> {
override val tag: PluginTag = PluginTag("controls.vision") override val tag: PluginTag = PluginTag("controls.vision")

View File

@ -13,6 +13,8 @@ import space.kscience.plotly.PlotlyConfig
import space.kscience.visionforge.html.HtmlVisionFragment import space.kscience.visionforge.html.HtmlVisionFragment
import space.kscience.visionforge.html.VisionPage import space.kscience.visionforge.html.VisionPage
import space.kscience.visionforge.html.VisionTagConsumer import space.kscience.visionforge.html.VisionTagConsumer
import space.kscience.visionforge.markup.MarkupPlugin
import space.kscience.visionforge.plotly.PlotlyPlugin
import space.kscience.visionforge.plotly.plotly import space.kscience.visionforge.plotly.plotly
import space.kscience.visionforge.server.VisionRoute import space.kscience.visionforge.server.VisionRoute
import space.kscience.visionforge.server.close import space.kscience.visionforge.server.close
@ -25,19 +27,27 @@ public fun Context.showDashboard(
routes: Routing.() -> Unit = {}, routes: Routing.() -> Unit = {},
configurationBuilder: VisionRoute.() -> Unit = {}, configurationBuilder: VisionRoute.() -> Unit = {},
visionFragment: HtmlVisionFragment, visionFragment: HtmlVisionFragment,
): ApplicationEngine = embeddedServer(CIO, port = port) { ): ApplicationEngine {
//create a sub-context for visualization
val visualisationContext = buildContext {
plugin(PlotlyPlugin)
plugin(ControlVisionPlugin)
plugin(MarkupPlugin)
}
return visualisationContext.embeddedServer(CIO, port = port) {
routing { routing {
staticResources("", null, null) staticResources("", null, null)
routes() routes()
} }
visionPage( visionPage(
visionManager, visualisationContext.visionManager,
VisionPage.scriptHeader("js/controls-vision.js"), VisionPage.scriptHeader("js/controls-vision.js"),
configurationBuilder = configurationBuilder, configurationBuilder = configurationBuilder,
visionFragment = visionFragment visionFragment = visionFragment
) )
}.also { }.also {
it.start(false) it.start(false)
it.openInBrowser() it.openInBrowser()
@ -48,6 +58,7 @@ public fun Context.showDashboard(
} }
it.close() it.close()
}
} }
context(VisionTagConsumer<*>) context(VisionTagConsumer<*>)

View File

@ -14,8 +14,10 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp 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.launch import space.kscience.controls.constructor.DeviceConstructor
import space.kscience.controls.constructor.* import space.kscience.controls.constructor.DoubleInRangeState
import space.kscience.controls.constructor.device
import space.kscience.controls.constructor.deviceProperty
import space.kscience.controls.constructor.library.* import space.kscience.controls.constructor.library.*
import space.kscience.controls.manager.ClockManager import space.kscience.controls.manager.ClockManager
import space.kscience.controls.manager.DeviceManager import space.kscience.controls.manager.DeviceManager
@ -40,77 +42,41 @@ import kotlin.time.DurationUnit
class LinearDrive( class LinearDrive(
drive: Drive,
start: LimitSwitch,
end: LimitSwitch,
pidParameters: PidParameters,
meta: Meta = Meta.EMPTY,
) : DeviceConstructor(drive.context, meta) {
val drive: Drive by device(drive)
val pid by device(PidRegulator(drive, pidParameters))
val start by device(start)
val end by device(end)
val position by deviceProperty(drive, Drive.position, Double.NaN)
val target by deviceProperty(pid, Regulator.target, 0.0)
}
/**
* A shortcut to create a virtual [LimitSwitch] from [DoubleInRangeState]
*/
fun LinearDrive(
context: Context, context: Context,
state: DoubleRangeState, positionState: DoubleInRangeState,
mass: Double, mass: Double,
pidParameters: PidParameters, pidParameters: PidParameters,
meta: Meta = Meta.EMPTY, meta: Meta = Meta.EMPTY,
) : DeviceConstructor(context, meta) { ): LinearDrive = LinearDrive(
drive = VirtualDrive(context, mass, positionState),
start = VirtualLimitSwitch(context, positionState.atStart),
end = VirtualLimitSwitch(context, positionState.atEnd),
pidParameters = pidParameters,
meta = meta
)
val drive by device(VirtualDrive.factory(mass, state))
val pid by device(PidRegulator(drive, pidParameters))
val start by device(LimitSwitch(state.atStartState))
val end by device(LimitSwitch(state.atEndState))
val positionState: DoubleRangeState by property(state)
private val targetState: MutableDeviceState<Double> by deviceProperty(pid, Regulator.target, 0.0)
var target: Double by targetState
}
private fun Context.launchPidDevice(
state: DoubleRangeState,
pidParameters: PidParameters,
mass: Double,
) = launch {
val device = install(
"device",
LinearDrive(this@launchPidDevice, state, mass, pidParameters)
).apply {
val clock = context.clock
val clockStart = clock.now()
doRecurring(10.milliseconds) {
val timeFromStart = clock.now() - clockStart
val t = timeFromStart.toDouble(DurationUnit.SECONDS)
val freq = 0.1
target = 5 * sin(2.0 * PI * freq * t) +
sin(2 * PI * 21 * freq * t + 0.02 * (timeFromStart / pidParameters.timeStep))
}
}
val maxAge = 10.seconds
showDashboard {
plot {
plotNumberState(context, state, maxAge = maxAge, sampling = 50.milliseconds) {
name = "real position"
}
plotDeviceProperty(device.pid, Regulator.position.name, maxAge = maxAge, sampling = 50.milliseconds) {
name = "read position"
}
plotDeviceProperty(device.pid, Regulator.target.name, maxAge = maxAge, sampling = 50.milliseconds) {
name = "target"
}
}
plot {
plotDeviceProperty(device.start, LimitSwitch.locked.name, maxAge = maxAge, sampling = 50.milliseconds) {
name = "start measured"
mode = ScatterMode.markers
}
plotDeviceProperty(device.end, LimitSwitch.locked.name, maxAge = maxAge, sampling = 50.milliseconds) {
name = "end measured"
mode = ScatterMode.markers
}
}
}
}
fun main() = application { fun main() = application {
val context = Context { val context = Context {
@ -140,12 +106,57 @@ fun main() = application {
) )
} }
context.launchPidDevice( val state = DoubleInRangeState(0.0, -6.0..6.0)
DoubleRangeState(0.0, -6.0..6.0),
pidParameters, val linearDrive = context.install(
mass = 0.05 "linearDrive",
LinearDrive(context, state, 0.05, pidParameters)
) )
val clockStart = context.clock.now()
linearDrive.doRecurring(10.milliseconds) {
val timeFromStart = clock.now() - clockStart
val t = timeFromStart.toDouble(DurationUnit.SECONDS)
val freq = 0.1
target.value = 5 * sin(2.0 * PI * freq * t) +
sin(2 * PI * 21 * freq * t + 0.02 * (timeFromStart / pidParameters.timeStep))
}
val maxAge = 10.seconds
context.showDashboard {
plot {
plotNumberState(context, state, maxAge = maxAge, sampling = 50.milliseconds) {
name = "real position"
}
plotDeviceProperty(linearDrive.pid, Regulator.position.name, maxAge = maxAge, sampling = 50.milliseconds) {
name = "read position"
}
plotDeviceProperty(linearDrive.pid, Regulator.target.name, maxAge = maxAge, sampling = 50.milliseconds) {
name = "target"
}
}
plot {
plotDeviceProperty(
linearDrive.start,
LimitSwitch.locked.name,
maxAge = maxAge,
sampling = 50.milliseconds
) {
name = "start measured"
mode = ScatterMode.markers
}
plotDeviceProperty(linearDrive.end, LimitSwitch.locked.name, maxAge = maxAge, sampling = 50.milliseconds) {
name = "end measured"
mode = ScatterMode.markers
}
}
}
Window(title = "Pid regulator simulator", onCloseRequest = ::exitApplication) { Window(title = "Pid regulator simulator", onCloseRequest = ::exitApplication) {
MaterialTheme { MaterialTheme {
Column { Column {

View File

@ -97,7 +97,7 @@ fun AxisPane(axes: Map<String, PiMotionMasterDevice.Axis>) {
@Composable @Composable
fun PiMotionMasterApp(device: PiMotionMasterDevice) { fun PiMotionMasterApp(device: PiMotionMasterDevice) {
val scope = rememberCoroutineScope() // val scope = rememberCoroutineScope()
val connected by device.composeState(PiMotionMasterDevice.connected, false) val connected by device.composeState(PiMotionMasterDevice.connected, false)
var debugServerJob by remember { mutableStateOf<Job?>(null) } var debugServerJob by remember { mutableStateOf<Job?>(null) }
var axes by remember { mutableStateOf<Map<String, PiMotionMasterDevice.Axis>?>(null) } var axes by remember { mutableStateOf<Map<String, PiMotionMasterDevice.Axis>?>(null) }

View File

@ -1,30 +0,0 @@
## Artifact:
The Maven coordinates of this project are `${group}:${name}:${version}`.
**Gradle:**
```groovy
repositories {
maven { url 'https://repo.kotlin.link' }
mavenCentral()
// development and snapshot versions
maven { url 'https://maven.pkg.jetbrains.space/spc/p/sci/dev' }
}
dependencies {
implementation '${group}:${name}:${version}'
}
```
**Gradle Kotlin DSL:**
```kotlin
repositories {
maven("https://repo.kotlin.link")
mavenCentral()
// development and snapshot versions
maven("https://maven.pkg.jetbrains.space/spc/p/sci/dev")
}
dependencies {
implementation("${group}:${name}:${version}")
}
```

View File

@ -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.3.0`. The Maven coordinates of this project are `space.kscience:magix-api:0.4.0-dev-1`.
**Gradle Kotlin DSL:** **Gradle Kotlin DSL:**
```kotlin ```kotlin
@ -16,6 +16,6 @@ repositories {
} }
dependencies { dependencies {
implementation("space.kscience:magix-api:0.3.0") implementation("space.kscience:magix-api:0.4.0-dev-1")
} }
``` ```

View File

@ -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.3.0`. The Maven coordinates of this project are `space.kscience:magix-java-endpoint:0.4.0-dev-1`.
**Gradle Kotlin DSL:** **Gradle Kotlin DSL:**
```kotlin ```kotlin
@ -16,6 +16,6 @@ repositories {
} }
dependencies { dependencies {
implementation("space.kscience:magix-java-endpoint:0.3.0") implementation("space.kscience:magix-java-endpoint:0.4.0-dev-1")
} }
``` ```

View File

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

View File

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

View File

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

View File

@ -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.3.0`. The Maven coordinates of this project are `space.kscience:magix-server:0.4.0-dev-1`.
**Gradle Kotlin DSL:** **Gradle Kotlin DSL:**
```kotlin ```kotlin
@ -16,6 +16,6 @@ repositories {
} }
dependencies { dependencies {
implementation("space.kscience:magix-server:0.3.0") implementation("space.kscience:magix-server:0.4.0-dev-1")
} }
``` ```

View File

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

View File

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

View File

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