Dev #8

Merged
altavir merged 78 commits from dev into master 2023-03-04 16:47:55 +03:00
137 changed files with 3685 additions and 2519 deletions

3
.gitmodules vendored Normal file
View File

@ -0,0 +1,3 @@
[submodule "magix/rfc"]
path = magix/rfc
url = https://github.com/waltz-controls/rfc

1
.space/CODEOWNERS Normal file
View File

@ -0,0 +1 @@
./space/* "Project Admin"

View File

@ -1,19 +1,38 @@
import space.kscience.gradle.isInDevelopment
import space.kscience.gradle.useApache2Licence
import space.kscience.gradle.useSPCTeam
plugins {
id("ru.mipt.npm.gradle.project")
id("space.kscience.gradle.project")
}
val dataforgeVersion: String by extra("0.5.1")
val ktorVersion: String by extra(ru.mipt.npm.gradle.KScienceVersions.ktorVersion)
val rsocketVersion by extra("0.13.1")
val dataforgeVersion: String by extra("0.6.1-dev-4")
val ktorVersion: String by extra(space.kscience.gradle.KScienceVersions.ktorVersion)
val rsocketVersion by extra("0.15.4")
val xodusVersion by extra("2.0.1")
allprojects {
group = "ru.mipt.npm"
version = "0.1.1"
group = "space.kscience"
version = "0.1.1-SNAPSHOT"
repositories{
maven("https://maven.pkg.jetbrains.space/spc/p/sci/dev")
}
}
ksciencePublish {
github("controls.kt")
space()
pom("https://github.com/SciProgCentre/controls.kt") {
useApache2Licence()
useSPCTeam()
}
github("controls.kt", "SciProgCentre")
space(
if (isInDevelopment) {
"https://maven.pkg.jetbrains.space/spc/p/sci/dev"
} else {
"https://maven.pkg.jetbrains.space/spc/p/sci/release"
}
)
space("https://maven.pkg.jetbrains.space/spc/p/controls/maven")
}
apiValidation {

View File

@ -1,24 +1,20 @@
plugins {
id("ru.mipt.npm.gradle.mpp")
id("space.kscience.gradle.mpp")
`maven-publish`
}
val dataforgeVersion: String by rootProject.extra
kscience {
useCoroutines("1.4.1")
jvm()
js()
native()
useCoroutines()
useSerialization{
json()
}
}
kotlin {
sourceSets {
commonMain{
dependencies {
api("space.kscience:dataforge-io:$dataforgeVersion")
api(npm.kotlinx.datetime)
}
}
dependencies {
api("space.kscience:dataforge-io:$dataforgeVersion")
api(npmlibs.kotlinx.datetime)
}
}
}

View File

@ -1,10 +0,0 @@
package ru.mipt.npm.controls.base
import ru.mipt.npm.controls.api.ActionDescriptor
import space.kscience.dataforge.meta.Meta
public interface DeviceAction {
public val name: String
public val descriptor: ActionDescriptor
public suspend operator fun invoke(arg: Meta? = null): Meta?
}

View File

@ -1,252 +0,0 @@
package ru.mipt.npm.controls.base
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import ru.mipt.npm.controls.api.*
import space.kscience.dataforge.context.Context
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.misc.DFExperimental
import kotlin.collections.set
import kotlin.coroutines.CoroutineContext
//TODO move to DataForge-core
@DFExperimental
public data class LogEntry(val content: String, val priority: Int = 0)
@OptIn(ExperimentalCoroutinesApi::class)
private open class BasicReadOnlyDeviceProperty(
val device: DeviceBase,
override val name: String,
default: Meta?,
override val descriptor: PropertyDescriptor,
private val getter: suspend (before: Meta?) -> Meta,
) : ReadOnlyDeviceProperty {
override val scope: CoroutineScope get() = device
private val state: MutableStateFlow<Meta?> = MutableStateFlow(default)
override val value: Meta? get() = state.value
override suspend fun invalidate() {
state.value = null
}
override fun updateLogical(item: Meta) {
state.value = item
scope.launch {
device.sharedMessageFlow.emit(
PropertyChangedMessage(
property = name,
value = item,
)
)
}
}
override suspend fun read(force: Boolean): Meta {
//backup current value
val currentValue = value
return if (force || currentValue == null) {
//all device operations should be run on device context
//propagate error, but do not fail scope
val res = withContext(scope.coroutineContext + SupervisorJob(scope.coroutineContext[Job])) {
getter(currentValue)
}
updateLogical(res)
res
} else {
currentValue
}
}
override fun flow(): StateFlow<Meta?> = state
}
@OptIn(ExperimentalCoroutinesApi::class)
private class BasicDeviceProperty(
device: DeviceBase,
name: String,
default: Meta?,
descriptor: PropertyDescriptor,
getter: suspend (Meta?) -> Meta,
private val setter: suspend (oldValue: Meta?, newValue: Meta) -> Meta?,
) : BasicReadOnlyDeviceProperty(device, name, default, descriptor, getter), DeviceProperty {
override var value: Meta?
get() = super.value
set(value) {
scope.launch {
if (value == null) {
invalidate()
} else {
write(value)
}
}
}
private val writeLock = Mutex()
override suspend fun write(item: Meta) {
writeLock.withLock {
//fast return if value is not changed
if (item == value) return@withLock
val oldValue = value
//all device operations should be run on device context
withContext(scope.coroutineContext + SupervisorJob(scope.coroutineContext[Job])) {
setter(oldValue, item)?.let {
updateLogical(it)
}
}
}
}
}
/**
* Baseline implementation of [Device] interface
*/
@Suppress("EXPERIMENTAL_API_USAGE")
public abstract class DeviceBase(final override val context: Context) : Device {
override val coroutineContext: CoroutineContext =
context.coroutineContext + SupervisorJob(context.coroutineContext[Job])
private val _properties = HashMap<String, ReadOnlyDeviceProperty>()
public val properties: Map<String, ReadOnlyDeviceProperty> get() = _properties
private val _actions = HashMap<String, DeviceAction>()
public val actions: Map<String, DeviceAction> get() = _actions
internal val sharedMessageFlow = MutableSharedFlow<DeviceMessage>()
override val messageFlow: SharedFlow<DeviceMessage> get() = sharedMessageFlow
private val sharedLogFlow = MutableSharedFlow<LogEntry>()
/**
* The [SharedFlow] of log messages
*/
@DFExperimental
public val logFlow: SharedFlow<LogEntry>
get() = sharedLogFlow
protected suspend fun log(message: String, priority: Int = 0) {
sharedLogFlow.emit(LogEntry(message, priority))
}
override val propertyDescriptors: Collection<PropertyDescriptor>
get() = _properties.values.map { it.descriptor }
override val actionDescriptors: Collection<ActionDescriptor>
get() = _actions.values.map { it.descriptor }
private fun <P : ReadOnlyDeviceProperty> registerProperty(name: String, property: P) {
if (_properties.contains(name)) error("Property with name $name already registered")
_properties[name] = property
}
internal fun registerAction(name: String, action: DeviceAction) {
if (_actions.contains(name)) error("Action with name $name already registered")
_actions[name] = action
}
override suspend fun readProperty(propertyName: String): Meta =
(_properties[propertyName] ?: error("Property with name $propertyName not defined")).read()
override fun getProperty(propertyName: String): Meta? =
(_properties[propertyName] ?: error("Property with name $propertyName not defined")).value
override suspend fun invalidate(propertyName: String) {
(_properties[propertyName] ?: error("Property with name $propertyName not defined")).invalidate()
}
override suspend fun writeProperty(propertyName: String, value: Meta) {
(_properties[propertyName] as? DeviceProperty ?: error("Property with name $propertyName not defined")).write(
value
)
}
override suspend fun execute(action: String, argument: Meta?): Meta? =
(_actions[action] ?: error("Request with name $action not defined")).invoke(argument)
/**
* Create a bound read-only property with given [getter]
*/
public fun createReadOnlyProperty(
name: String,
default: Meta?,
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
getter: suspend (Meta?) -> Meta,
): ReadOnlyDeviceProperty {
val property = BasicReadOnlyDeviceProperty(
this,
name,
default,
PropertyDescriptor(name).apply(descriptorBuilder),
getter
)
registerProperty(name, property)
return property
}
/**
* Create a bound mutable property with given [getter] and [setter]
*/
internal fun createMutableProperty(
name: String,
default: Meta?,
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
getter: suspend (Meta?) -> Meta,
setter: suspend (oldValue: Meta?, newValue: Meta) -> Meta?,
): DeviceProperty {
val property = BasicDeviceProperty(
this,
name,
default,
PropertyDescriptor(name).apply(descriptorBuilder),
getter,
setter
)
registerProperty(name, property)
return property
}
/**
* A stand-alone action
*/
private inner class BasicDeviceAction(
override val name: String,
override val descriptor: ActionDescriptor,
private val block: suspend (Meta?) -> Meta?,
) : DeviceAction {
override suspend fun invoke(arg: Meta?): Meta? =
withContext(coroutineContext) {
block(arg)
}
}
/**
* Create a new bound action
*/
internal fun createAction(
name: String,
descriptorBuilder: ActionDescriptor.() -> Unit = {},
block: suspend (Meta?) -> Meta?,
): DeviceAction {
val action = BasicDeviceAction(name, ActionDescriptor(name).apply(descriptorBuilder), block)
registerAction(name, action)
return action
}
public companion object {
}
}

View File

@ -1,74 +0,0 @@
package ru.mipt.npm.controls.base
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.Flow
import ru.mipt.npm.controls.api.PropertyDescriptor
import space.kscience.dataforge.meta.Meta
import kotlin.time.Duration
/**
* Read-only device property
*/
public interface ReadOnlyDeviceProperty {
/**
* Property name, should be unique in device
*/
public val name: String
/**
* Property descriptor
*/
public val descriptor: PropertyDescriptor
public val scope: CoroutineScope
/**
* Erase logical value and force re-read from device on next [read]
*/
public suspend fun invalidate()
/**
* Directly update property logical value and notify listener without writing it to device
*/
public fun updateLogical(item: Meta)
/**
* Get cached value and return null if value is invalid or not initialized
*/
public val value: Meta?
/**
* Read value either from cache if cache is valid or directly from physical device.
* If [force], reread from physical state even if the logical state is set.
*/
public suspend fun read(force: Boolean = false): Meta
/**
* The [Flow] representing future logical states of the property.
* Produces null when the state is invalidated
*/
public fun flow(): Flow<Meta?>
}
/**
* Launch recurring force re-read job on a property scope with given [duration] between reads.
*/
public fun ReadOnlyDeviceProperty.readEvery(duration: Duration): Job = scope.launch {
while (isActive) {
read(true)
delay(duration)
}
}
/**
* A writeable device property with non-suspended write
*/
public interface DeviceProperty : ReadOnlyDeviceProperty {
override var value: Meta?
/**
* Write value to physical device. Invalidates logical value, but does not update it automatically
*/
public suspend fun write(item: Meta)
}

View File

@ -1,58 +0,0 @@
package ru.mipt.npm.controls.base
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.transformations.MetaConverter
/**
* A type-safe wrapper on top of read-only property
*/
public open class TypedReadOnlyDeviceProperty<T : Any>(
private val property: ReadOnlyDeviceProperty,
protected val converter: MetaConverter<T>,
) : ReadOnlyDeviceProperty by property {
public fun updateLogical(obj: T) {
property.updateLogical(converter.objectToMeta(obj))
}
public open val typedValue: T? get() = value?.let { converter.metaToObject(it) }
public suspend fun readTyped(force: Boolean = false): T {
val meta = read(force)
return converter.metaToObject(meta)
?: error("Meta $meta could not be converted by $converter")
}
public fun flowTyped(): Flow<T?> = flow().map { it?.let { converter.metaToObject(it) } }
}
/**
* A type-safe wrapper for a read-write device property
*/
public class TypedDeviceProperty<T : Any>(
private val property: DeviceProperty,
converter: MetaConverter<T>,
) : TypedReadOnlyDeviceProperty<T>(property, converter), DeviceProperty {
override var value: Meta?
get() = property.value
set(arg) {
property.value = arg
}
public override var typedValue: T?
get() = value?.let { converter.metaToObject(it) }
set(arg) {
property.value = arg?.let { converter.objectToMeta(arg) }
}
override suspend fun write(item: Meta) {
property.write(item)
}
public suspend fun write(obj: T) {
property.write(converter.objectToMeta(obj))
}
}

View File

@ -1,58 +0,0 @@
package ru.mipt.npm.controls.base
import ru.mipt.npm.controls.api.ActionDescriptor
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.MutableMeta
import space.kscience.dataforge.values.Value
import kotlin.properties.PropertyDelegateProvider
import kotlin.properties.ReadOnlyProperty
import kotlin.reflect.KProperty
private fun <D : DeviceBase> D.provideAction(): ReadOnlyProperty<D, DeviceAction> =
ReadOnlyProperty { _: D, property: KProperty<*> ->
val name = property.name
return@ReadOnlyProperty actions[name]!!
}
public typealias ActionDelegate = ReadOnlyProperty<DeviceBase, DeviceAction>
private class ActionProvider<D : DeviceBase>(
val owner: D,
val descriptorBuilder: ActionDescriptor.() -> Unit = {},
val block: suspend (Meta?) -> Meta?,
) : PropertyDelegateProvider<D, ActionDelegate> {
override operator fun provideDelegate(thisRef: D, property: KProperty<*>): ActionDelegate {
val name = property.name
owner.createAction(name, descriptorBuilder, block)
return owner.provideAction()
}
}
public fun DeviceBase.requesting(
descriptorBuilder: ActionDescriptor.() -> Unit = {},
action: suspend (Meta?) -> Meta?,
): PropertyDelegateProvider<DeviceBase, ActionDelegate> = ActionProvider(this, descriptorBuilder, action)
public fun <D : DeviceBase> D.requestingValue(
descriptorBuilder: ActionDescriptor.() -> Unit = {},
action: suspend (Meta?) -> Any?,
): PropertyDelegateProvider<D, ActionDelegate> = ActionProvider(this, descriptorBuilder) {
val res = action(it)
Meta(Value.of(res))
}
public fun <D : DeviceBase> D.requestingMeta(
descriptorBuilder: ActionDescriptor.() -> Unit = {},
action: suspend MutableMeta.(Meta?) -> Unit,
): PropertyDelegateProvider<D, ActionDelegate> = ActionProvider(this, descriptorBuilder) {
Meta { action(it) }
}
public fun DeviceBase.acting(
descriptorBuilder: ActionDescriptor.() -> Unit = {},
action: suspend (Meta?) -> Unit,
): PropertyDelegateProvider<DeviceBase, ActionDelegate> = ActionProvider(this, descriptorBuilder) {
action(it)
null
}

View File

@ -1,283 +0,0 @@
package ru.mipt.npm.controls.base
import ru.mipt.npm.controls.api.PropertyDescriptor
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.MutableMeta
import space.kscience.dataforge.meta.boolean
import space.kscience.dataforge.meta.double
import space.kscience.dataforge.meta.transformations.MetaConverter
import space.kscience.dataforge.values.Null
import space.kscience.dataforge.values.Value
import space.kscience.dataforge.values.asValue
import kotlin.properties.PropertyDelegateProvider
import kotlin.properties.ReadOnlyProperty
import kotlin.reflect.KProperty
private fun <D : DeviceBase> D.provideProperty(name: String): ReadOnlyProperty<D, ReadOnlyDeviceProperty> =
ReadOnlyProperty { _: D, _: KProperty<*> ->
return@ReadOnlyProperty properties.getValue(name)
}
private fun <D : DeviceBase, T : Any> D.provideProperty(
name: String,
converter: MetaConverter<T>,
): ReadOnlyProperty<D, TypedReadOnlyDeviceProperty<T>> =
ReadOnlyProperty { _: D, _: KProperty<*> ->
return@ReadOnlyProperty TypedReadOnlyDeviceProperty(properties.getValue(name), converter)
}
public typealias ReadOnlyPropertyDelegate = ReadOnlyProperty<DeviceBase, ReadOnlyDeviceProperty>
public typealias TypedReadOnlyPropertyDelegate<T> = ReadOnlyProperty<DeviceBase, TypedReadOnlyDeviceProperty<T>>
private class ReadOnlyDevicePropertyProvider<D : DeviceBase>(
val owner: D,
val default: Meta?,
val descriptorBuilder: PropertyDescriptor.() -> Unit = {},
private val getter: suspend (Meta?) -> Meta,
) : PropertyDelegateProvider<D, ReadOnlyPropertyDelegate> {
override operator fun provideDelegate(thisRef: D, property: KProperty<*>): ReadOnlyPropertyDelegate {
val name = property.name
owner.createReadOnlyProperty(name, default, descriptorBuilder, getter)
return owner.provideProperty(name)
}
}
private class TypedReadOnlyDevicePropertyProvider<D : DeviceBase, T : Any>(
val owner: D,
val default: Meta?,
val converter: MetaConverter<T>,
val descriptorBuilder: PropertyDescriptor.() -> Unit = {},
private val getter: suspend (Meta?) -> Meta,
) : PropertyDelegateProvider<D, TypedReadOnlyPropertyDelegate<T>> {
override operator fun provideDelegate(thisRef: D, property: KProperty<*>): TypedReadOnlyPropertyDelegate<T> {
val name = property.name
owner.createReadOnlyProperty(name, default, descriptorBuilder, getter)
return owner.provideProperty(name, converter)
}
}
public fun DeviceBase.reading(
default: Meta? = null,
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
getter: suspend (Meta?) -> Meta,
): PropertyDelegateProvider<DeviceBase, ReadOnlyPropertyDelegate> = ReadOnlyDevicePropertyProvider(
this,
default,
descriptorBuilder,
getter
)
public fun DeviceBase.readingValue(
default: Value? = null,
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
getter: suspend () -> Any?,
): PropertyDelegateProvider<DeviceBase, ReadOnlyPropertyDelegate> = ReadOnlyDevicePropertyProvider(
this,
default?.let { Meta(it) },
descriptorBuilder,
getter = { Meta(Value.of(getter())) }
)
public fun DeviceBase.readingNumber(
default: Number? = null,
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
getter: suspend () -> Number,
): PropertyDelegateProvider<DeviceBase, TypedReadOnlyPropertyDelegate<Number>> = TypedReadOnlyDevicePropertyProvider(
this,
default?.let { Meta(it.asValue()) },
MetaConverter.number,
descriptorBuilder,
getter = {
val number = getter()
Meta(number.asValue())
}
)
public fun DeviceBase.readingDouble(
default: Number? = null,
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
getter: suspend () -> Double,
): PropertyDelegateProvider<DeviceBase, TypedReadOnlyPropertyDelegate<Double>> = TypedReadOnlyDevicePropertyProvider(
this,
default?.let { Meta(it.asValue()) },
MetaConverter.double,
descriptorBuilder,
getter = {
val number = getter()
Meta(number.asValue())
}
)
public fun DeviceBase.readingString(
default: String? = null,
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
getter: suspend () -> String,
): PropertyDelegateProvider<DeviceBase, TypedReadOnlyPropertyDelegate<String>> = TypedReadOnlyDevicePropertyProvider(
this,
default?.let { Meta(it.asValue()) },
MetaConverter.string,
descriptorBuilder,
getter = {
val number = getter()
Meta(number.asValue())
}
)
public fun DeviceBase.readingBoolean(
default: Boolean? = null,
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
getter: suspend () -> Boolean,
): PropertyDelegateProvider<DeviceBase, TypedReadOnlyPropertyDelegate<Boolean>> = TypedReadOnlyDevicePropertyProvider(
this,
default?.let { Meta(it.asValue()) },
MetaConverter.boolean,
descriptorBuilder,
getter = {
val boolean = getter()
Meta(boolean.asValue())
}
)
public fun DeviceBase.readingMeta(
default: Meta? = null,
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
getter: suspend MutableMeta.() -> Unit,
): PropertyDelegateProvider<DeviceBase, TypedReadOnlyPropertyDelegate<Meta>> = TypedReadOnlyDevicePropertyProvider(
this,
default,
MetaConverter.meta,
descriptorBuilder,
getter = {
Meta { getter() }
}
)
private fun DeviceBase.provideMutableProperty(name: String): ReadOnlyProperty<DeviceBase, DeviceProperty> =
ReadOnlyProperty { _: DeviceBase, _: KProperty<*> ->
return@ReadOnlyProperty properties[name] as DeviceProperty
}
private fun <T : Any> DeviceBase.provideMutableProperty(
name: String,
converter: MetaConverter<T>,
): ReadOnlyProperty<DeviceBase, TypedDeviceProperty<T>> =
ReadOnlyProperty { _: DeviceBase, _: KProperty<*> ->
return@ReadOnlyProperty TypedDeviceProperty(properties[name] as DeviceProperty, converter)
}
public typealias PropertyDelegate = ReadOnlyProperty<DeviceBase, DeviceProperty>
public typealias TypedPropertyDelegate<T> = ReadOnlyProperty<DeviceBase, TypedDeviceProperty<T>>
private class DevicePropertyProvider<D : DeviceBase>(
val owner: D,
val default: Meta?,
val descriptorBuilder: PropertyDescriptor.() -> Unit = {},
private val getter: suspend (Meta?) -> Meta,
private val setter: suspend (oldValue: Meta?, newValue: Meta) -> Meta?,
) : PropertyDelegateProvider<D, PropertyDelegate> {
override operator fun provideDelegate(thisRef: D, property: KProperty<*>): PropertyDelegate {
val name = property.name
owner.createMutableProperty(name, default, descriptorBuilder, getter, setter)
return owner.provideMutableProperty(name)
}
}
private class TypedDevicePropertyProvider<D : DeviceBase, T : Any>(
val owner: D,
val default: Meta?,
val converter: MetaConverter<T>,
val descriptorBuilder: PropertyDescriptor.() -> Unit = {},
private val getter: suspend (Meta?) -> Meta,
private val setter: suspend (oldValue: Meta?, newValue: Meta) -> Meta?,
) : PropertyDelegateProvider<D, TypedPropertyDelegate<T>> {
override operator fun provideDelegate(thisRef: D, property: KProperty<*>): TypedPropertyDelegate<T> {
val name = property.name
owner.createMutableProperty(name, default, descriptorBuilder, getter, setter)
return owner.provideMutableProperty(name, converter)
}
}
public fun DeviceBase.writing(
default: Meta? = null,
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
getter: suspend (Meta?) -> Meta,
setter: suspend (oldValue: Meta?, newValue: Meta) -> Meta?,
): PropertyDelegateProvider<DeviceBase, PropertyDelegate> = DevicePropertyProvider(
this,
default,
descriptorBuilder,
getter,
setter
)
public fun DeviceBase.writingVirtual(
default: Meta,
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
): PropertyDelegateProvider<DeviceBase, PropertyDelegate> = writing(
default,
descriptorBuilder,
getter = { it ?: default },
setter = { _, newItem -> newItem }
)
public fun DeviceBase.writingVirtual(
default: Value,
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
): PropertyDelegateProvider<DeviceBase, PropertyDelegate> = writing(
Meta(default),
descriptorBuilder,
getter = { it ?: Meta(default) },
setter = { _, newItem -> newItem }
)
public fun <D : DeviceBase> D.writingDouble(
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
getter: suspend (Double) -> Double,
setter: suspend (oldValue: Double?, newValue: Double) -> Double?,
): PropertyDelegateProvider<D, TypedPropertyDelegate<Double>> {
val innerGetter: suspend (Meta?) -> Meta = {
Meta(getter(it.double ?: Double.NaN).asValue())
}
val innerSetter: suspend (oldValue: Meta?, newValue: Meta) -> Meta? = { oldValue, newValue ->
setter(oldValue.double, newValue.double ?: Double.NaN)?.asMeta()
}
return TypedDevicePropertyProvider(
this,
Meta(Double.NaN.asValue()),
MetaConverter.double,
descriptorBuilder,
innerGetter,
innerSetter
)
}
public fun <D : DeviceBase> D.writingBoolean(
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
getter: suspend (Boolean?) -> Boolean,
setter: suspend (oldValue: Boolean?, newValue: Boolean) -> Boolean?,
): PropertyDelegateProvider<D, TypedPropertyDelegate<Boolean>> {
val innerGetter: suspend (Meta?) -> Meta = {
Meta(getter(it.boolean).asValue())
}
val innerSetter: suspend (oldValue: Meta?, newValue: Meta) -> Meta? = { oldValue, newValue ->
setter(oldValue.boolean, newValue.boolean ?: error("Can't convert $newValue to boolean"))?.asValue()
?.let { Meta(it) }
}
return TypedDevicePropertyProvider(
this,
Meta(Null),
MetaConverter.boolean,
descriptorBuilder,
innerGetter,
innerSetter
)
}

View File

@ -1,34 +0,0 @@
package ru.mipt.npm.controls.ports
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
/**
* A port handler for synchronous (request-response) communication with a port. Only one request could be active at a time (others are suspended.
* The handler does not guarantee exclusive access to the port so the user mush ensure that no other controller handles port at the moment.
*
*/
public class SynchronousPortHandler(public val port: Port) {
private val mutex = Mutex()
/**
* Send a single message and wait for the flow of respond messages.
*/
public suspend fun <R> respond(data: ByteArray, transform: suspend Flow<ByteArray>.() -> R): R {
return mutex.withLock {
port.send(data)
transform(port.receiving())
}
}
}
/**
* Send request and read incoming data blocks until the delimiter is encountered
*/
public suspend fun SynchronousPortHandler.respondWithDelimiter(data: ByteArray, delimiter: ByteArray): ByteArray {
return respond(data) {
withDelimiter(delimiter).first()
}
}

View File

@ -1,85 +0,0 @@
package ru.mipt.npm.controls.properties
import ru.mipt.npm.controls.api.ActionDescriptor
import ru.mipt.npm.controls.api.Device
import ru.mipt.npm.controls.api.PropertyDescriptor
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.transformations.MetaConverter
/**
* This API is internal and should not be used in user code
*/
@RequiresOptIn
public annotation class InternalDeviceAPI
public interface DevicePropertySpec<in D : Device, T> {
/**
* Property name, should be unique in device
*/
public val name: String
/**
* Property descriptor
*/
public val descriptor: PropertyDescriptor
/**
* Meta item converter for resulting type
*/
public val converter: MetaConverter<T>
/**
* Read physical value from the given [device]
*/
@InternalDeviceAPI
public suspend fun read(device: D): T
}
@OptIn(InternalDeviceAPI::class)
public suspend fun <D : Device, T> DevicePropertySpec<D, T>.readMeta(device: D): Meta =
converter.objectToMeta(read(device))
public interface WritableDevicePropertySpec<in D : Device, T> : DevicePropertySpec<D, T> {
/**
* Write physical value to a device
*/
@InternalDeviceAPI
public suspend fun write(device: D, value: T)
}
@OptIn(InternalDeviceAPI::class)
public suspend fun <D : Device, T> WritableDevicePropertySpec<D, T>.writeMeta(device: D, item: Meta) {
write(device, converter.metaToObject(item) ?: error("Meta $item could not be read with $converter"))
}
public interface DeviceActionSpec<in D : Device, I, O> {
/**
* Action name, should be unique in device
*/
public val name: String
/**
* Action descriptor
*/
public val descriptor: ActionDescriptor
public val inputConverter: MetaConverter<I>
public val outputConverter: MetaConverter<O>
/**
* Execute action on a device
*/
public suspend fun execute(device: D, input: I?): O?
}
public suspend fun <D : Device, I, O> DeviceActionSpec<D, I, O>.executeMeta(
device: D,
item: Meta?
): Meta? {
val arg = item?.let { inputConverter.metaToObject(item) }
val res = execute(device, arg)
return res?.let { outputConverter.objectToMeta(res) }
}

View File

@ -1,4 +1,4 @@
package ru.mipt.npm.controls.api
package space.kscience.controls.api
import io.ktor.utils.io.core.Closeable
import kotlinx.coroutines.CoroutineScope
@ -8,7 +8,7 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import ru.mipt.npm.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.meta.Meta
import space.kscience.dataforge.misc.Type
@ -21,6 +21,12 @@ import space.kscience.dataforge.names.Name
*/
@Type(DEVICE_TARGET)
public interface Device : Closeable, ContextAware, CoroutineScope {
/**
* Initial configuration meta for the device
*/
public val meta: Meta get() = Meta.EMPTY
/**
* List of supported property descriptors
*/
@ -51,13 +57,13 @@ public interface Device : Closeable, ContextAware, CoroutineScope {
/**
* Set property [value] for a property with name [propertyName].
* In rare cases could suspend if the [Device] supports command queue and it is full at the moment.
* In rare cases could suspend if the [Device] supports command queue, and it is full at the moment.
*/
public suspend fun writeProperty(propertyName: String, value: Meta)
/**
* A subscription-based [Flow] of [DeviceMessage] provided by device. The flow is guaranteed to be readable
* multiple times
* multiple times.
*/
public val messageFlow: Flow<DeviceMessage>
@ -67,6 +73,14 @@ public interface Device : Closeable, ContextAware, CoroutineScope {
*/
public suspend fun execute(action: String, argument: Meta? = null): Meta?
/**
* Initialize the device. This function suspends until the device is finished initialization
*/
public suspend fun open(): Unit = Unit
/**
* Close and terminate the device. This function does not wait for device to be closed.
*/
override fun close() {
cancel("The device is closed")
}
@ -85,9 +99,9 @@ public suspend fun Device.getOrReadProperty(propertyName: String): Meta =
/**
* Get a snapshot of logical state of the device
*
* TODO currently this
* TODO currently this
*/
public fun Device.getProperties(): Meta = Meta {
public fun Device.getAllProperties(): Meta = Meta {
for (descriptor in propertyDescriptors) {
setMeta(Name.parse(descriptor.name), getProperty(descriptor.name))
}
@ -97,4 +111,4 @@ public fun Device.getProperties(): Meta = Meta {
* Subscribe on property changes for the whole device
*/
public fun Device.onPropertyChange(callback: suspend PropertyChangedMessage.() -> Unit): Job =
messageFlow.filterIsInstance<PropertyChangedMessage>().onEach(callback).launchIn(this)
messageFlow.filterIsInstance<PropertyChangedMessage>().onEach(callback).launchIn(this)

View File

@ -1,4 +1,4 @@
package ru.mipt.npm.controls.api
package space.kscience.controls.api
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.names.*

View File

@ -1,4 +1,4 @@
package ru.mipt.npm.controls.api
package space.kscience.controls.api
import kotlinx.datetime.Clock
import kotlinx.datetime.Instant
@ -7,7 +7,7 @@ import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.decodeFromJsonElement
import kotlinx.serialization.json.encodeToJsonElement
import space.kscience.dataforge.io.SimpleEnvelope
import space.kscience.dataforge.io.Envelope
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.toJson
import space.kscience.dataforge.meta.toMeta
@ -113,6 +113,8 @@ public data class GetDescriptionMessage(
@SerialName("description")
public data class DescriptionMessage(
val description: Meta,
val properties: Collection<PropertyDescriptor>,
val actions: Collection<ActionDescriptor>,
override val sourceDevice: Name,
override val targetDevice: Name? = null,
override val comment: String? = null,
@ -219,4 +221,4 @@ public data class DeviceErrorMessage(
public fun DeviceMessage.toMeta(): Meta = Json.encodeToJsonElement(this).toMeta()
public fun DeviceMessage.toEnvelope(): SimpleEnvelope = SimpleEnvelope(toMeta(), null)
public fun DeviceMessage.toEnvelope(): Envelope = Envelope(toMeta(), null)

View File

@ -1,14 +1,13 @@
package ru.mipt.npm.controls.api
package space.kscience.controls.api
import io.ktor.utils.io.core.Closeable
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
/**
* A generic bi-directional sender/receiver object
* A generic bidirectional sender/receiver object
*/
public interface Socket<T> : Closeable {
/**

View File

@ -1,4 +1,4 @@
package ru.mipt.npm.controls.api
package space.kscience.controls.api
import kotlinx.serialization.Serializable
import space.kscience.dataforge.meta.descriptors.MetaDescriptor

View File

@ -1,7 +1,8 @@
package ru.mipt.npm.controls.controllers
package space.kscience.controls.manager
import ru.mipt.npm.controls.api.Device
import ru.mipt.npm.controls.api.DeviceHub
import kotlinx.coroutines.launch
import space.kscience.controls.api.Device
import space.kscience.controls.api.DeviceHub
import space.kscience.dataforge.context.*
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.MutableMeta
@ -30,7 +31,7 @@ public class DeviceManager : AbstractPlugin(), DeviceHub {
override val tag: PluginTag = PluginTag("devices", group = PluginTag.DATAFORGE_GROUP)
override val type: KClass<out DeviceManager> = DeviceManager::class
override fun invoke(meta: Meta, context: Context): DeviceManager = DeviceManager()
override fun build(context: Context, meta: Meta): DeviceManager = DeviceManager()
}
}
@ -38,6 +39,9 @@ public class DeviceManager : AbstractPlugin(), DeviceHub {
public fun <D : Device> DeviceManager.install(name: String, factory: Factory<D>, meta: Meta = Meta.EMPTY): D {
val device = factory(meta, context)
registerDevice(NameToken(name), device)
device.launch {
device.open()
}
return device
}

View File

@ -1,15 +1,11 @@
package ru.mipt.npm.controls.controllers
package space.kscience.controls.manager
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.encodeToJsonElement
import ru.mipt.npm.controls.api.*
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.toMeta
import space.kscience.controls.api.*
import space.kscience.dataforge.names.Name
import space.kscience.dataforge.names.plus
@ -48,21 +44,10 @@ public suspend fun Device.respondMessage(deviceTarget: Name, request: DeviceMess
}
is GetDescriptionMessage -> {
val descriptionMeta = Meta {
"properties" put {
propertyDescriptors.map { descriptor ->
descriptor.name put Json.encodeToJsonElement(descriptor).toMeta()
}
}
"actions" put {
actionDescriptors.map { descriptor ->
descriptor.name put Json.encodeToJsonElement(descriptor).toMeta()
}
}
}
DescriptionMessage(
description = descriptionMeta,
description = meta,
properties = propertyDescriptors,
actions = actionDescriptors,
sourceDevice = deviceTarget,
targetDevice = request.sourceDevice
)
@ -95,6 +80,8 @@ public suspend fun DeviceHub.respondHubMessage(request: DeviceMessage): DeviceMe
* Collect all messages from given [DeviceHub], applying proper relative names
*/
public fun DeviceHub.hubMessageFlow(scope: CoroutineScope): Flow<DeviceMessage> {
//TODO could we avoid using downstream scope?
val outbox = MutableSharedFlow<DeviceMessage>()
if (this is Device) {
messageFlow.onEach {

View File

@ -1,16 +1,24 @@
package ru.mipt.npm.controls.ports
package space.kscience.controls.ports
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.receiveAsFlow
import ru.mipt.npm.controls.api.Socket
import space.kscience.controls.api.Socket
import space.kscience.dataforge.context.*
import space.kscience.dataforge.misc.Type
import kotlin.coroutines.CoroutineContext
public interface Port : ContextAware, Socket<ByteArray>
public typealias PortFactory = Factory<Port>
@Type(PortFactory.TYPE)
public interface PortFactory: Factory<Port>{
public val type: String
public companion object{
public const val TYPE: String = "controls.port"
}
}
public abstract class AbstractPort(
override val context: Context,
@ -64,12 +72,10 @@ public abstract class AbstractPort(
/**
* Raw flow of incoming data chunks. The chunks are not guaranteed to be complete phrases.
* In order to form phrases some condition should used on top of it.
* In order to form phrases some condition should be used on top of it.
* For example [delimitedIncoming] generates phrases with fixed delimiter.
*/
override fun receiving(): Flow<ByteArray> {
return incoming.receiveAsFlow()
}
override fun receiving(): Flow<ByteArray> = incoming.receiveAsFlow()
override fun close() {
outgoing.close()

View File

@ -1,8 +1,7 @@
package ru.mipt.npm.controls.ports
package space.kscience.controls.ports
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex

View File

@ -0,0 +1,34 @@
package space.kscience.controls.ports
import space.kscience.dataforge.context.*
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.string
import kotlin.reflect.KClass
public class Ports : AbstractPlugin() {
override val tag: PluginTag get() = Companion.tag
private val portFactories by lazy {
context.gather<PortFactory>(PortFactory.TYPE)
}
private val portCache = mutableMapOf<Meta, Port>()
public fun buildPort(meta: Meta): Port = portCache.getOrPut(meta) {
val type by meta.string { error("Port type is not defined") }
val factory = portFactories.values.firstOrNull { it.type == type }
?: error("Port factory for type $type not found")
factory.build(context, meta)
}
public companion object : PluginFactory<Ports> {
override val tag: PluginTag = PluginTag("controls.ports", group = PluginTag.DATAFORGE_GROUP)
override val type: KClass<out Ports> = Ports::class
override fun build(context: Context, meta: Meta): Ports = Ports()
}
}

View File

@ -0,0 +1,42 @@
package space.kscience.controls.ports
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
/**
* A port handler for synchronous (request-response) communication with a port. Only one request could be active at a time (others are suspended.
* The handler does not guarantee exclusive access to the port so the user mush ensure that no other controller handles port at the moment.
*/
public class SynchronousPort(public val port: Port, private val mutex: Mutex) : Port by port {
/**
* Send a single message and wait for the flow of respond messages.
*/
public suspend fun <R> respond(data: ByteArray, transform: suspend Flow<ByteArray>.() -> R): R = mutex.withLock {
port.send(data)
transform(port.receiving())
}
}
/**
* Provide a synchronous wrapper for a port
*/
public fun Port.synchronous(mutex: Mutex = Mutex()): SynchronousPort = SynchronousPort(this, mutex)
/**
* Send request and read incoming data blocks until the delimiter is encountered
*/
public suspend fun SynchronousPort.respondWithDelimiter(
data: ByteArray,
delimiter: ByteArray,
): ByteArray = respond(data) {
withDelimiter(delimiter).first()
}
public suspend fun SynchronousPort.respondStringWithDelimiter(
data: String,
delimiter: String,
): String = respond(data.encodeToByteArray()) {
withStringDelimiter(delimiter).first()
}

View File

@ -1,4 +1,4 @@
package ru.mipt.npm.controls.ports
package space.kscience.controls.ports
import io.ktor.utils.io.core.BytePacketBuilder
import io.ktor.utils.io.core.readBytes
@ -10,10 +10,10 @@ import kotlinx.coroutines.flow.transform
/**
* Transform byte fragments into complete phrases using given delimiter. Not thread safe.
*/
public fun Flow<ByteArray>.withDelimiter(delimiter: ByteArray, expectedMessageSize: Int = 32): Flow<ByteArray> {
public fun Flow<ByteArray>.withDelimiter(delimiter: ByteArray): Flow<ByteArray> {
require(delimiter.isNotEmpty()) { "Delimiter must not be empty" }
val output = BytePacketBuilder(expectedMessageSize)
val output = BytePacketBuilder()
var matcherPosition = 0
return transform { chunk ->
@ -40,12 +40,11 @@ public fun Flow<ByteArray>.withDelimiter(delimiter: ByteArray, expectedMessageSi
/**
* Transform byte fragments into utf-8 phrases using utf-8 delimiter
*/
public fun Flow<ByteArray>.withDelimiter(delimiter: String, expectedMessageSize: Int = 32): Flow<String> {
return withDelimiter(delimiter.encodeToByteArray(), expectedMessageSize).map { it.decodeToString() }
public fun Flow<ByteArray>.withStringDelimiter(delimiter: String): Flow<String> {
return withDelimiter(delimiter.encodeToByteArray()).map { it.decodeToString() }
}
/**
* A flow of delimited phrases
*/
public suspend fun Port.delimitedIncoming(delimiter: ByteArray, expectedMessageSize: Int = 32): Flow<ByteArray> =
receiving().withDelimiter(delimiter, expectedMessageSize)
public fun Port.delimitedIncoming(delimiter: ByteArray): Flow<ByteArray> = receiving().withDelimiter(delimiter)

View File

@ -1,4 +1,4 @@
package ru.mipt.npm.controls.properties
package space.kscience.controls.spec
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
@ -7,30 +7,28 @@ import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import ru.mipt.npm.controls.api.*
import space.kscience.controls.api.*
import space.kscience.dataforge.context.Context
import space.kscience.dataforge.context.Global
import space.kscience.dataforge.meta.Meta
import kotlin.coroutines.CoroutineContext
/**
* A device generated from specification
* @param D recursive self-type for properties and actions
*/
@OptIn(InternalDeviceAPI::class)
public open class DeviceBySpec<D : DeviceBySpec<D>>(
public val spec: DeviceSpec<D>,
context: Context = Global,
meta: Meta = Meta.EMPTY
public abstract class DeviceBase<D : DeviceBase<D>>(
override val context: Context = Global,
override val meta: Meta = Meta.EMPTY,
) : Device {
override var context: Context = context
internal set
public var meta: Meta = meta
internal set
/**
* Collection of property specifications
*/
public abstract val properties: Map<String, DevicePropertySpec<D, *>>
public val properties: Map<String, DevicePropertySpec<D, *>> get() = spec.properties
public val actions: Map<String, DeviceActionSpec<D, *, *>> get() = spec.actions
/**
* Collection of action specifications
*/
public abstract val actions: Map<String, DeviceActionSpec<D, *, *>>
override val propertyDescriptors: Collection<PropertyDescriptor>
get() = properties.values.map { it.descriptor }
@ -42,6 +40,9 @@ public open class DeviceBySpec<D : DeviceBySpec<D>>(
context.coroutineContext + SupervisorJob(context.coroutineContext[Job])
}
/**
* Logical state store
*/
private val logicalState: HashMap<String, Meta?> = HashMap()
private val sharedMessageFlow: MutableSharedFlow<DeviceMessage> = MutableSharedFlow()
@ -68,6 +69,13 @@ public open class DeviceBySpec<D : DeviceBySpec<D>>(
}
}
/**
* Update logical state using given [spec] and its convertor
*/
protected suspend fun <T> updateLogical(spec: DevicePropertySpec<D, T>, value: T) {
updateLogical(spec.name, spec.converter.objectToMeta(value))
}
/**
* Force read physical value and push an update if it is changed. It does not matter if logical state is present.
* The logical state is updated after read
@ -98,17 +106,21 @@ public open class DeviceBySpec<D : DeviceBySpec<D>>(
}
override suspend fun execute(action: String, argument: Meta?): Meta? =
actions[action]?.executeMeta(self, argument)
actions[action]?.executeWithMeta(self, argument)
/**
* Read typed value and update/push event if needed
* Read typed value and update/push event if needed.
* Return null if property read is not successful or property is undefined.
*/
public suspend fun <T> DevicePropertySpec<D, T>.read(): T {
val res = read(self)
public suspend fun <T> DevicePropertySpec<D, T>.readOrNull(): T? {
val res = read(self) ?: return null
updateLogical(name, converter.objectToMeta(res))
return res
}
public suspend fun <T> DevicePropertySpec<D, T>.read(): T =
readOrNull() ?: error("Failed to read property $name state")
public fun <T> DevicePropertySpec<D, T>.get(): T? = getProperty(name)?.let(converter::metaToObject)
/**
@ -123,19 +135,37 @@ public open class DeviceBySpec<D : DeviceBySpec<D>>(
}
}
override fun close() {
with(spec) { self.onShutdown() }
/**
* Reset logical state of a property
*/
public suspend fun DevicePropertySpec<D, *>.invalidate() {
invalidate(name)
}
public suspend operator fun <I, O> DeviceActionSpec<D, I, O>.invoke(input: I? = null): O? = execute(self, input)
}
/**
* A device generated from specification
* @param D recursive self-type for properties and actions
*/
public open class DeviceBySpec<D : DeviceBySpec<D>>(
public val spec: DeviceSpec<in D>,
context: Context = Global,
meta: Meta = Meta.EMPTY,
) : DeviceBase<D>(context, meta) {
override val properties: Map<String, DevicePropertySpec<D, *>> get() = spec.properties
override val actions: Map<String, DeviceActionSpec<D, *, *>> get() = spec.actions
override suspend fun open(): Unit = with(spec) {
super.open()
self.onOpen()
}
override fun close(): Unit = with(spec) {
self.onClose()
super.close()
}
}
public suspend fun <D : DeviceBySpec<D>, T : Any> D.read(
propertySpec: DevicePropertySpec<D, T>
): T = propertySpec.read()
public fun <D : DeviceBySpec<D>, T> D.write(
propertySpec: WritableDevicePropertySpec<D, T>,
value: T
): Job = launch {
propertySpec.write(value)
}

View File

@ -0,0 +1,15 @@
package space.kscience.controls.spec
import space.kscience.controls.api.Device
import space.kscience.controls.api.PropertyDescriptor
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.transformations.MetaConverter
internal object DeviceMetaPropertySpec: DevicePropertySpec<Device,Meta> {
override val descriptor: PropertyDescriptor = PropertyDescriptor("@meta")
override val converter: MetaConverter<Meta> = MetaConverter.meta
@InternalDeviceAPI
override suspend fun read(device: Device): Meta = device.meta
}

View File

@ -0,0 +1,129 @@
package space.kscience.controls.spec
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import space.kscience.controls.api.ActionDescriptor
import space.kscience.controls.api.Device
import space.kscience.controls.api.PropertyChangedMessage
import space.kscience.controls.api.PropertyDescriptor
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.transformations.MetaConverter
/**
* This API is internal and should not be used in user code
*/
@RequiresOptIn("This API should not be called outside of Device internals")
public annotation class InternalDeviceAPI
public interface DevicePropertySpec<in D : Device, T> {
/**
* Property descriptor
*/
public val descriptor: PropertyDescriptor
/**
* Meta item converter for resulting type
*/
public val converter: MetaConverter<T>
/**
* Read physical value from the given [device]
*/
@InternalDeviceAPI
public suspend fun read(device: D): T?
}
/**
* Property name, should be unique in device
*/
public val DevicePropertySpec<*, *>.name: String get() = descriptor.name
@OptIn(InternalDeviceAPI::class)
public suspend fun <D : Device, T> DevicePropertySpec<D, T>.readMeta(device: D): Meta? =
read(device)?.let(converter::objectToMeta)
public interface WritableDevicePropertySpec<in D : Device, T> : DevicePropertySpec<D, T> {
/**
* Write physical value to a device
*/
@InternalDeviceAPI
public suspend fun write(device: D, value: T)
}
@OptIn(InternalDeviceAPI::class)
public suspend fun <D : Device, T> WritableDevicePropertySpec<D, T>.writeMeta(device: D, item: Meta) {
write(device, converter.metaToObject(item) ?: error("Meta $item could not be read with $converter"))
}
public interface DeviceActionSpec<in D : Device, I, O> {
/**
* Action descriptor
*/
public val descriptor: ActionDescriptor
public val inputConverter: MetaConverter<I>
public val outputConverter: MetaConverter<O>
/**
* Execute action on a device
*/
public suspend fun execute(device: D, input: I?): O?
}
/**
* Action name, should be unique in device
*/
public val DeviceActionSpec<*, *, *>.name: String get() = descriptor.name
public suspend fun <D : Device, I, O> DeviceActionSpec<D, I, O>.executeWithMeta(
device: D,
item: Meta?,
): Meta? {
val arg = item?.let { inputConverter.metaToObject(item) }
val res = execute(device, arg)
return res?.let { outputConverter.objectToMeta(res) }
}
public suspend fun <D : DeviceBase<D>, T : Any> D.read(
propertySpec: DevicePropertySpec<D, T>,
): T = propertySpec.read()
public suspend fun <D : Device, T : Any> D.read(
propertySpec: DevicePropertySpec<D, T>,
): T = propertySpec.converter.metaToObject(readProperty(propertySpec.name))
?: error("Property meta converter returned null")
public fun <D : Device, T> D.write(
propertySpec: WritableDevicePropertySpec<D, T>,
value: T,
): Job = launch {
writeProperty(propertySpec.name, propertySpec.converter.objectToMeta(value))
}
public fun <D : DeviceBase<D>, T> D.write(
propertySpec: WritableDevicePropertySpec<D, T>,
value: T,
): Job = launch {
propertySpec.write(value)
}
/**
* A type safe property change listener
*/
public fun <D : Device, T> Device.onPropertyChange(
spec: DevicePropertySpec<D, T>,
callback: suspend PropertyChangedMessage.(T?) -> Unit,
): Job = messageFlow
.filterIsInstance<PropertyChangedMessage>()
.filter { it.property == spec.name }
.onEach { change ->
change.callback(spec.converter.metaToObject(change.value))
}.launchIn(this)

View File

@ -1,10 +1,9 @@
package ru.mipt.npm.controls.properties
package space.kscience.controls.spec
import kotlinx.coroutines.withContext
import ru.mipt.npm.controls.api.ActionDescriptor
import ru.mipt.npm.controls.api.PropertyDescriptor
import space.kscience.dataforge.context.Context
import space.kscience.dataforge.context.Factory
import space.kscience.controls.api.ActionDescriptor
import space.kscience.controls.api.Device
import space.kscience.controls.api.PropertyDescriptor
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.transformations.MetaConverter
import kotlin.properties.PropertyDelegateProvider
@ -14,28 +13,37 @@ import kotlin.reflect.KProperty
import kotlin.reflect.KProperty1
@OptIn(InternalDeviceAPI::class)
public abstract class DeviceSpec<D : DeviceBySpec<D>>(
private val buildDevice: () -> D
) : Factory<D> {
private val _properties = HashMap<String, DevicePropertySpec<D, *>>()
public abstract class DeviceSpec<D : Device> {
//initializing meta property for everyone
private val _properties = hashMapOf<String, DevicePropertySpec<D, *>>(
DeviceMetaPropertySpec.name to DeviceMetaPropertySpec
)
public val properties: Map<String, DevicePropertySpec<D, *>> get() = _properties
private val _actions = HashMap<String, DeviceActionSpec<D, *, *>>()
public val actions: Map<String, DeviceActionSpec<D, *, *>> get() = _actions
public fun <T : Any, P : DevicePropertySpec<D, T>> registerProperty(deviceProperty: P): P {
public open suspend fun D.onOpen() {
}
public open fun D.onClose() {
}
public fun <T, P : DevicePropertySpec<D, T>> registerProperty(deviceProperty: P): P {
_properties[deviceProperty.name] = deviceProperty
return deviceProperty
}
public fun <T : Any> registerProperty(
public fun <T> registerProperty(
converter: MetaConverter<T>,
readOnlyProperty: KProperty1<D, T>,
descriptorBuilder: PropertyDescriptor.() -> Unit = {}
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
): DevicePropertySpec<D, T> {
val deviceProperty = object : DevicePropertySpec<D, T> {
override val name: String = readOnlyProperty.name
override val descriptor: PropertyDescriptor = PropertyDescriptor(this.name).apply(descriptorBuilder)
override val descriptor: PropertyDescriptor =
PropertyDescriptor(readOnlyProperty.name).apply(descriptorBuilder)
override val converter: MetaConverter<T> = converter
override suspend fun read(device: D): T =
withContext(device.coroutineContext) { readOnlyProperty.get(device) }
@ -43,16 +51,39 @@ public abstract class DeviceSpec<D : DeviceBySpec<D>>(
return registerProperty(deviceProperty)
}
public fun <T : Any> property(
public fun <T> property(
converter: MetaConverter<T>,
readOnlyProperty: KProperty1<D, T>,
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<Any?, DevicePropertySpec<D, T>>> =
PropertyDelegateProvider { _, property ->
val deviceProperty = object : DevicePropertySpec<D, T> {
override val descriptor: PropertyDescriptor = PropertyDescriptor(property.name).apply {
//TODO add type from converter
writable = true
}.apply(descriptorBuilder)
override val converter: MetaConverter<T> = converter
override suspend fun read(device: D): T = withContext(device.coroutineContext) {
readOnlyProperty.get(device)
}
}
registerProperty(deviceProperty)
ReadOnlyProperty { _, _ ->
deviceProperty
}
}
public fun <T> mutableProperty(
converter: MetaConverter<T>,
readWriteProperty: KMutableProperty1<D, T>,
descriptorBuilder: PropertyDescriptor.() -> Unit = {}
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<Any?, WritableDevicePropertySpec<D, T>>> =
PropertyDelegateProvider { _, property ->
val deviceProperty = object : WritableDevicePropertySpec<D, T> {
override val name: String = property.name
override val descriptor: PropertyDescriptor = PropertyDescriptor(name).apply {
override val descriptor: PropertyDescriptor = PropertyDescriptor(property.name).apply {
//TODO add type from converter
writable = true
}.apply(descriptorBuilder)
@ -73,20 +104,19 @@ public abstract class DeviceSpec<D : DeviceBySpec<D>>(
}
}
public fun <T : Any> property(
public fun <T> property(
converter: MetaConverter<T>,
name: String? = null,
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
read: suspend D.() -> T
name: String? = null,
read: suspend D.() -> T?,
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DevicePropertySpec<D, T>>> =
PropertyDelegateProvider { _: DeviceSpec<D>, property ->
val propertyName = name ?: property.name
val deviceProperty = object : DevicePropertySpec<D, T> {
override val name: String = propertyName
override val descriptor: PropertyDescriptor = PropertyDescriptor(this.name).apply(descriptorBuilder)
override val descriptor: PropertyDescriptor = PropertyDescriptor(propertyName).apply(descriptorBuilder)
override val converter: MetaConverter<T> = converter
override suspend fun read(device: D): T = withContext(device.coroutineContext) { device.read() }
override suspend fun read(device: D): T? = withContext(device.coroutineContext) { device.read() }
}
registerProperty(deviceProperty)
ReadOnlyProperty<DeviceSpec<D>, DevicePropertySpec<D, T>> { _, _ ->
@ -94,21 +124,20 @@ public abstract class DeviceSpec<D : DeviceBySpec<D>>(
}
}
public fun <T : Any> property(
public fun <T> mutableProperty(
converter: MetaConverter<T>,
name: String? = null,
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
read: suspend D.() -> T,
write: suspend D.(T) -> Unit
name: String? = null,
read: suspend D.() -> T?,
write: suspend D.(T) -> Unit,
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, WritableDevicePropertySpec<D, T>>> =
PropertyDelegateProvider { _: DeviceSpec<D>, property: KProperty<*> ->
val propertyName = name ?: property.name
val deviceProperty = object : WritableDevicePropertySpec<D, T> {
override val name: String = propertyName
override val descriptor: PropertyDescriptor = PropertyDescriptor(this.name).apply(descriptorBuilder)
override val descriptor: PropertyDescriptor = PropertyDescriptor(propertyName).apply(descriptorBuilder)
override val converter: MetaConverter<T> = converter
override suspend fun read(device: D): T = withContext(device.coroutineContext) { device.read() }
override suspend fun read(device: D): T? = withContext(device.coroutineContext) { device.read() }
override suspend fun write(device: D, value: T): Unit = withContext(device.coroutineContext) {
device.write(value)
@ -121,22 +150,21 @@ public abstract class DeviceSpec<D : DeviceBySpec<D>>(
}
public fun <I : Any, O : Any> registerAction(deviceAction: DeviceActionSpec<D, I, O>): DeviceActionSpec<D, I, O> {
public fun <I, O> registerAction(deviceAction: DeviceActionSpec<D, I, O>): DeviceActionSpec<D, I, O> {
_actions[deviceAction.name] = deviceAction
return deviceAction
}
public fun <I : Any, O : Any> action(
public fun <I, O> action(
inputConverter: MetaConverter<I>,
outputConverter: MetaConverter<O>,
name: String? = null,
descriptorBuilder: ActionDescriptor.() -> Unit = {},
execute: suspend D.(I?) -> O?
name: String? = null,
execute: suspend D.(I?) -> O?,
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DeviceActionSpec<D, I, O>>> =
PropertyDelegateProvider { _: DeviceSpec<D>, property ->
val actionName = name ?: property.name
val deviceAction = object : DeviceActionSpec<D, I, O> {
override val name: String = actionName
override val descriptor: ActionDescriptor = ActionDescriptor(actionName).apply(descriptorBuilder)
override val inputConverter: MetaConverter<I> = inputConverter
@ -153,19 +181,68 @@ public abstract class DeviceSpec<D : DeviceBySpec<D>>(
}
/**
* The function is executed right after device initialization is finished
* An action that takes [Meta] and returns [Meta]. No conversions are done
*/
public open fun D.onStartup() {}
public fun metaAction(
descriptorBuilder: ActionDescriptor.() -> Unit = {},
name: String? = null,
execute: suspend D.(Meta?) -> Meta?,
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DeviceActionSpec<D, Meta, Meta>>> =
action(
MetaConverter.Companion.meta,
MetaConverter.Companion.meta,
descriptorBuilder,
name
) {
execute(it)
}
/**
* The function is executed before device is shut down
* An action that takes no parameters and returns no values
*/
public open fun D.onShutdown() {}
override fun invoke(meta: Meta, context: Context): D = buildDevice().apply {
this.context = context
this.meta = meta
onStartup()
}
public fun unitAction(
descriptorBuilder: ActionDescriptor.() -> Unit = {},
name: String? = null,
execute: suspend D.() -> Unit,
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DeviceActionSpec<D, Meta, Meta>>> =
action(
MetaConverter.Companion.meta,
MetaConverter.Companion.meta,
descriptorBuilder,
name
) {
execute()
null
}
}
/**
* Register a mutable logical property for a device
*/
@OptIn(InternalDeviceAPI::class)
public fun <T, D : DeviceBase<D>> DeviceSpec<D>.logicalProperty(
converter: MetaConverter<T>,
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
name: String? = null,
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<Any?, WritableDevicePropertySpec<D, T>>> =
PropertyDelegateProvider { _, property ->
val deviceProperty = object : WritableDevicePropertySpec<D, T> {
val propertyName = name ?: property.name
override val descriptor: PropertyDescriptor = PropertyDescriptor(propertyName).apply {
//TODO add type from converter
writable = true
}.apply(descriptorBuilder)
override val converter: MetaConverter<T> = converter
override suspend fun read(device: D): T? = device.getProperty(propertyName)?.let(converter::metaToObject)
override suspend fun write(device: D, value: T): Unit =
device.writeProperty(propertyName, converter.objectToMeta(value))
}
registerProperty(deviceProperty)
ReadOnlyProperty { _, _ ->
deviceProperty
}
}

View File

@ -1,4 +1,4 @@
package ru.mipt.npm.controls.properties
package space.kscience.controls.spec
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.Flow
@ -14,7 +14,7 @@ import kotlin.time.Duration
*
* The flow is canceled when the device scope is canceled
*/
public fun <D : DeviceBySpec<D>, R> D.readRecurring(interval: Duration, reader: suspend D.() -> R): Flow<R> = flow {
public fun <D : DeviceBase<D>, R> D.readRecurring(interval: Duration, reader: suspend D.() -> R): Flow<R> = flow {
while (isActive) {
kotlinx.coroutines.delay(interval)
emit(reader())
@ -22,9 +22,9 @@ public fun <D : DeviceBySpec<D>, R> D.readRecurring(interval: Duration, reader:
}
/**
* Do a recurring task on a device. The task could
* Do a recurring (with a fixed delay) task on a device.
*/
public fun <D : DeviceBySpec<D>> D.doRecurring(interval: Duration, task: suspend D.() -> Unit): Job = launch {
public fun <D : DeviceBase<D>> D.doRecurring(interval: Duration, task: suspend D.() -> Unit): Job = launch {
while (isActive) {
kotlinx.coroutines.delay(interval)
task()

View File

@ -1,12 +1,7 @@
package ru.mipt.npm.controls.base
package space.kscience.controls.spec
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.double
import space.kscience.dataforge.meta.enum
import space.kscience.dataforge.meta.get
import space.kscience.dataforge.meta.*
import space.kscience.dataforge.meta.transformations.MetaConverter
import space.kscience.dataforge.values.asValue
import space.kscience.dataforge.values.double
import kotlin.time.Duration
import kotlin.time.DurationUnit
import kotlin.time.toDuration

View File

@ -1,28 +1,28 @@
package ru.mipt.npm.controls.properties
package space.kscience.controls.spec
import ru.mipt.npm.controls.api.PropertyDescriptor
import ru.mipt.npm.controls.api.metaDescriptor
import space.kscience.controls.api.PropertyDescriptor
import space.kscience.controls.api.metaDescriptor
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.ValueType
import space.kscience.dataforge.meta.transformations.MetaConverter
import space.kscience.dataforge.values.ValueType
import kotlin.properties.PropertyDelegateProvider
import kotlin.properties.ReadOnlyProperty
//read only delegates
public fun <D : DeviceBySpec<D>> DeviceSpec<D>.booleanProperty(
name: String? = null,
public fun <D : DeviceBase<D>> DeviceSpec<D>.booleanProperty(
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
read: suspend D.() -> Boolean
name: String? = null,
read: suspend D.() -> Boolean?
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DevicePropertySpec<D, Boolean>>> = property(
MetaConverter.boolean,
name,
{
metaDescriptor {
type(ValueType.BOOLEAN)
}
descriptorBuilder()
},
name,
read
)
@ -35,110 +35,110 @@ private inline fun numberDescriptor(
descriptorBuilder()
}
public fun <D : DeviceBySpec<D>> DeviceSpec<D>.numberProperty(
public fun <D : DeviceBase<D>> DeviceSpec<D>.numberProperty(
name: String? = null,
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
read: suspend D.() -> Number
read: suspend D.() -> Number?
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DevicePropertySpec<D, Number>>> = property(
MetaConverter.number,
name,
numberDescriptor(descriptorBuilder),
name,
read
)
public fun <D : DeviceBySpec<D>> DeviceSpec<D>.doubleProperty(
name: String? = null,
public fun <D : DeviceBase<D>> DeviceSpec<D>.doubleProperty(
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
read: suspend D.() -> Double
name: String? = null,
read: suspend D.() -> Double?
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DevicePropertySpec<D, Double>>> = property(
MetaConverter.double,
name,
numberDescriptor(descriptorBuilder),
name,
read
)
public fun <D : DeviceBySpec<D>> DeviceSpec<D>.stringProperty(
name: String? = null,
public fun <D : DeviceBase<D>> DeviceSpec<D>.stringProperty(
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
read: suspend D.() -> String
name: String? = null,
read: suspend D.() -> String?
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DevicePropertySpec<D, String>>> = property(
MetaConverter.string,
name,
{
metaDescriptor {
type(ValueType.STRING)
}
descriptorBuilder()
},
name,
read
)
public fun <D : DeviceBySpec<D>> DeviceSpec<D>.metaProperty(
name: String? = null,
public fun <D : DeviceBase<D>> DeviceSpec<D>.metaProperty(
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
read: suspend D.() -> Meta
name: String? = null,
read: suspend D.() -> Meta?
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DevicePropertySpec<D, Meta>>> = property(
MetaConverter.meta,
name,
{
metaDescriptor {
type(ValueType.STRING)
}
descriptorBuilder()
},
name,
read
)
//read-write delegates
public fun <D : DeviceBySpec<D>> DeviceSpec<D>.booleanProperty(
name: String? = null,
public fun <D : DeviceBase<D>> DeviceSpec<D>.booleanProperty(
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
read: suspend D.() -> Boolean,
name: String? = null,
read: suspend D.() -> Boolean?,
write: suspend D.(Boolean) -> Unit
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, WritableDevicePropertySpec<D, Boolean>>> =
property(
mutableProperty(
MetaConverter.boolean,
name,
{
metaDescriptor {
type(ValueType.BOOLEAN)
}
descriptorBuilder()
},
name,
read,
write
)
public fun <D : DeviceBySpec<D>> DeviceSpec<D>.numberProperty(
name: String? = null,
public fun <D : DeviceBase<D>> DeviceSpec<D>.numberProperty(
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
name: String? = null,
read: suspend D.() -> Number,
write: suspend D.(Number) -> Unit
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, WritableDevicePropertySpec<D, Number>>> =
property(MetaConverter.number, name, numberDescriptor(descriptorBuilder), read, write)
mutableProperty(MetaConverter.number, numberDescriptor(descriptorBuilder), name, read, write)
public fun <D : DeviceBySpec<D>> DeviceSpec<D>.doubleProperty(
name: String? = null,
public fun <D : DeviceBase<D>> DeviceSpec<D>.doubleProperty(
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
name: String? = null,
read: suspend D.() -> Double,
write: suspend D.(Double) -> Unit
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, WritableDevicePropertySpec<D, Double>>> =
property(MetaConverter.double, name, numberDescriptor(descriptorBuilder), read, write)
mutableProperty(MetaConverter.double, numberDescriptor(descriptorBuilder), name, read, write)
public fun <D : DeviceBySpec<D>> DeviceSpec<D>.stringProperty(
name: String? = null,
public fun <D : DeviceBase<D>> DeviceSpec<D>.stringProperty(
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
name: String? = null,
read: suspend D.() -> String,
write: suspend D.(String) -> Unit
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, WritableDevicePropertySpec<D, String>>> =
property(MetaConverter.string, name, descriptorBuilder, read, write)
mutableProperty(MetaConverter.string, descriptorBuilder, name, read, write)
public fun <D : DeviceBySpec<D>> DeviceSpec<D>.metaProperty(
name: String? = null,
public fun <D : DeviceBase<D>> DeviceSpec<D>.metaProperty(
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
name: String? = null,
read: suspend D.() -> Meta,
write: suspend D.(Meta) -> Unit
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, WritableDevicePropertySpec<D, Meta>>> =
property(MetaConverter.meta, name, descriptorBuilder, read, write)
mutableProperty(MetaConverter.meta, descriptorBuilder, name, read, write)

View File

@ -1,89 +0,0 @@
package ru.mipt.npm.controls.controllers
import kotlinx.coroutines.runBlocking
import ru.mipt.npm.controls.base.*
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.transformations.MetaConverter
import kotlin.properties.ReadOnlyProperty
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty
import kotlin.time.Duration
/**
* Blocking read of the value
*/
public operator fun ReadOnlyDeviceProperty.getValue(thisRef: Any?, property: KProperty<*>): Meta =
runBlocking(scope.coroutineContext) {
read()
}
public operator fun <T: Any> TypedReadOnlyDeviceProperty<T>.getValue(thisRef: Any?, property: KProperty<*>): T =
runBlocking(scope.coroutineContext) {
readTyped()
}
public operator fun DeviceProperty.setValue(thisRef: Any?, property: KProperty<*>, value: Meta) {
this.value = value
}
public operator fun <T: Any> TypedDeviceProperty<T>.setValue(thisRef: Any?, property: KProperty<*>, value: T) {
this.typedValue = value
}
public fun <T : Any> ReadOnlyDeviceProperty.convert(
metaConverter: MetaConverter<T>,
forceRead: Boolean,
): ReadOnlyProperty<Any?, T> {
return ReadOnlyProperty { _, _ ->
runBlocking(scope.coroutineContext) {
val meta = read(forceRead)
metaConverter.metaToObject(meta)?: error("Meta $meta could not be converted by $metaConverter")
}
}
}
public fun <T : Any> DeviceProperty.convert(
metaConverter: MetaConverter<T>,
forceRead: Boolean,
): ReadWriteProperty<Any?, T> {
return object : ReadWriteProperty<Any?, T> {
override fun getValue(thisRef: Any?, property: KProperty<*>): T = runBlocking(scope.coroutineContext) {
val meta = read(forceRead)
metaConverter.metaToObject(meta)?: error("Meta $meta could not be converted by $metaConverter")
}
override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
this@convert.setValue(thisRef, property, value.let { metaConverter.objectToMeta(it) })
}
}
}
public fun ReadOnlyDeviceProperty.double(forceRead: Boolean = false): ReadOnlyProperty<Any?, Double> =
convert(MetaConverter.double, forceRead)
public fun DeviceProperty.double(forceRead: Boolean = false): ReadWriteProperty<Any?, Double> =
convert(MetaConverter.double, forceRead)
public fun ReadOnlyDeviceProperty.int(forceRead: Boolean = false): ReadOnlyProperty<Any?, Int> =
convert(MetaConverter.int, forceRead)
public fun DeviceProperty.int(forceRead: Boolean = false): ReadWriteProperty<Any?, Int> =
convert(MetaConverter.int, forceRead)
public fun ReadOnlyDeviceProperty.string(forceRead: Boolean = false): ReadOnlyProperty<Any?, String> =
convert(MetaConverter.string, forceRead)
public fun DeviceProperty.string(forceRead: Boolean = false): ReadWriteProperty<Any?, String> =
convert(MetaConverter.string, forceRead)
public fun ReadOnlyDeviceProperty.boolean(forceRead: Boolean = false): ReadOnlyProperty<Any?, Boolean> =
convert(MetaConverter.boolean, forceRead)
public fun DeviceProperty.boolean(forceRead: Boolean = false): ReadWriteProperty<Any?, Boolean> =
convert(MetaConverter.boolean, forceRead)
public fun ReadOnlyDeviceProperty.duration(forceRead: Boolean = false): ReadOnlyProperty<Any?, Duration> =
convert(DurationConverter, forceRead)
public fun DeviceProperty.duration(forceRead: Boolean = false): ReadWriteProperty<Any?, Duration> =
convert(DurationConverter, forceRead)

View File

@ -1,10 +0,0 @@
package ru.mipt.npm.controls.properties
import kotlinx.coroutines.runBlocking
/**
* Blocking property get call
*/
public operator fun <D : DeviceBySpec<D>, T : Any> D.get(
propertySpec: DevicePropertySpec<D, T>
): T = runBlocking { read(propertySpec) }

View File

@ -1,9 +1,8 @@
package ru.mipt.npm.controls.misc
package space.kscience.controls.misc
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.get
import space.kscience.dataforge.meta.long
import space.kscience.dataforge.values.long
import java.time.Instant
// TODO move to core

View File

@ -1,4 +1,4 @@
package ru.mipt.npm.controls.ports
package space.kscience.controls.ports
import kotlinx.coroutines.*
import space.kscience.dataforge.context.Context
@ -52,19 +52,20 @@ public class TcpPort private constructor(
}
if (num < 0) cancel("The input channel is exhausted")
} catch (ex: Exception) {
logger.error(ex){"Channel read error"}
logger.error(ex) { "Channel read error" }
delay(1000)
}
}
}
override suspend fun write(data: ByteArray) {
override suspend fun write(data: ByteArray): Unit = withContext(Dispatchers.IO){
futureChannel.await().write(ByteBuffer.wrap(data))
}
@OptIn(ExperimentalCoroutinesApi::class)
override fun close() {
listenerJob.cancel()
if(futureChannel.isCompleted){
if (futureChannel.isCompleted) {
futureChannel.getCompleted().close()
} else {
futureChannel.cancel()
@ -73,6 +74,9 @@ public class TcpPort private constructor(
}
public companion object : PortFactory {
override val type: String = "tcp"
public fun open(
context: Context,
host: String,
@ -82,7 +86,7 @@ public class TcpPort private constructor(
return TcpPort(context, host, port, coroutineContext)
}
override fun invoke(meta: Meta, context: Context): Port {
override fun build(context: Context, meta: Meta): Port {
val host = meta["host"].string ?: "localhost"
val port = meta["port"].int ?: error("Port value for TCP port is not defined in $meta")
return open(context, host, port)

View File

@ -0,0 +1,30 @@
package space.kscience.controls.ports
import space.kscience.dataforge.context.AbstractPlugin
import space.kscience.dataforge.context.Context
import space.kscience.dataforge.context.PluginFactory
import space.kscience.dataforge.context.PluginTag
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.names.Name
import kotlin.reflect.KClass
public class TcpPortPlugin : AbstractPlugin() {
override val tag: PluginTag get() = Companion.tag
override fun content(target: String): Map<Name, Any> = when(target){
PortFactory.TYPE -> mapOf(Name.EMPTY to TcpPort)
else -> emptyMap()
}
public companion object : PluginFactory<TcpPortPlugin> {
override val tag: PluginTag = PluginTag("controls.ports.tcp", group = PluginTag.DATAFORGE_GROUP)
override val type: KClass<out TcpPortPlugin> = TcpPortPlugin::class
override fun build(context: Context, meta: Meta): TcpPortPlugin = TcpPortPlugin()
}
}

View File

@ -0,0 +1,10 @@
package space.kscience.controls.spec
import kotlinx.coroutines.runBlocking
/**
* Blocking property get call
*/
public operator fun <D : DeviceBase<D>, T : Any> D.get(
propertySpec: DevicePropertySpec<D, T>
): T? = runBlocking { read(propertySpec) }

View File

@ -1,4 +1,4 @@
package ru.mipt.npm.controls.ports
package space.kscience.controls.ports
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map

View File

@ -0,0 +1,10 @@
plugins {
id("space.kscience.gradle.jvm")
}
val ktorVersion: String by rootProject.extra
dependencies {
api(projects.controlsCore)
api("io.ktor:ktor-network:$ktorVersion")
}

View File

@ -1,4 +1,4 @@
package ru.mipt.npm.controls.ports
package space.kscience.controls.ports
import io.ktor.network.selector.ActorSelectorManager
import io.ktor.network.sockets.aSocket
@ -16,7 +16,6 @@ import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.get
import space.kscience.dataforge.meta.int
import space.kscience.dataforge.meta.string
import java.net.InetSocketAddress
import kotlin.coroutines.CoroutineContext
public class KtorTcpPort internal constructor(
@ -29,7 +28,7 @@ public class KtorTcpPort internal constructor(
override fun toString(): String = "port[tcp:$host:$port]"
private val futureSocket = scope.async {
aSocket(ActorSelectorManager(Dispatchers.IO)).tcp().connect(InetSocketAddress(host, port))
aSocket(ActorSelectorManager(Dispatchers.IO)).tcp().connect(host, port)
}
private val writeChannel = scope.async {
@ -38,7 +37,7 @@ public class KtorTcpPort internal constructor(
private val listenerJob = scope.launch {
val input = futureSocket.await().openReadChannel()
input.consumeEachBufferRange { buffer, last ->
input.consumeEachBufferRange { buffer, _ ->
val array = ByteArray(buffer.remaining())
buffer.get(array)
receive(array)
@ -56,7 +55,10 @@ public class KtorTcpPort internal constructor(
super.close()
}
public companion object: PortFactory {
public companion object : PortFactory {
override val type: String = "tcp"
public fun open(
context: Context,
host: String,
@ -66,7 +68,7 @@ public class KtorTcpPort internal constructor(
return KtorTcpPort(context, host, port, coroutineContext)
}
override fun invoke(meta: Meta, context: Context): Port {
override fun build(context: Context, meta: Meta): Port {
val host = meta["host"].string ?: "localhost"
val port = meta["port"].int ?: error("Port value for TCP port is not defined in $meta")
return open(context, host, port)

View File

@ -0,0 +1,30 @@
package space.kscience.controls.ports
import space.kscience.dataforge.context.AbstractPlugin
import space.kscience.dataforge.context.Context
import space.kscience.dataforge.context.PluginFactory
import space.kscience.dataforge.context.PluginTag
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.names.Name
import kotlin.reflect.KClass
public class KtorTcpPortPlugin : AbstractPlugin() {
override val tag: PluginTag get() = Companion.tag
override fun content(target: String): Map<Name, Any> = when(target){
PortFactory.TYPE -> mapOf(Name.EMPTY to KtorTcpPort)
else -> emptyMap()
}
public companion object : PluginFactory<KtorTcpPortPlugin> {
override val tag: PluginTag = PluginTag("controls.ports.serial", group = PluginTag.DATAFORGE_GROUP)
override val type: KClass<out KtorTcpPortPlugin> = KtorTcpPortPlugin::class
override fun build(context: Context, meta: Meta): KtorTcpPortPlugin = KtorTcpPortPlugin()
}
}

View File

@ -1,21 +1,16 @@
plugins {
id("ru.mipt.npm.gradle.mpp")
id("space.kscience.gradle.mpp")
`maven-publish`
}
kscience{
jvm()
js()
useSerialization {
json()
}
}
kotlin {
sourceSets {
commonMain {
dependencies {
implementation(project(":magix:magix-rsocket"))
implementation(project(":controls-core"))
}
}
dependencies {
implementation(project(":magix:magix-rsocket"))
implementation(project(":controls-core"))
}
}

View File

@ -1,64 +0,0 @@
package ru.mipt.npm.controls.client
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import ru.mipt.npm.controls.api.DeviceMessage
import ru.mipt.npm.controls.controllers.DeviceManager
import ru.mipt.npm.controls.controllers.hubMessageFlow
import ru.mipt.npm.controls.controllers.respondHubMessage
import ru.mipt.npm.magix.api.MagixEndpoint
import ru.mipt.npm.magix.api.MagixMessage
import space.kscience.dataforge.context.error
import space.kscience.dataforge.context.logger
public const val DATAFORGE_MAGIX_FORMAT: String = "dataforge"
internal fun generateId(request: MagixMessage<*>): String = if (request.id != null) {
"${request.id}.response"
} else {
"df[${request.payload.hashCode()}"
}
/**
* Communicate with server in [Magix format](https://github.com/waltz-controls/rfc/tree/master/1)
*/
public fun DeviceManager.connectToMagix(
endpoint: MagixEndpoint<DeviceMessage>,
endpointID: String = DATAFORGE_MAGIX_FORMAT,
): Job = context.launch {
endpoint.subscribe().onEach { request ->
val responsePayload = respondHubMessage(request.payload)
if (responsePayload != null) {
val response = MagixMessage(
format = DATAFORGE_MAGIX_FORMAT,
id = generateId(request),
parentId = request.id,
origin = endpointID,
payload = responsePayload
)
endpoint.broadcast(response)
}
}.catch { error ->
logger.error(error) { "Error while responding to message" }
}.launchIn(this)
hubMessageFlow(this).onEach { payload ->
endpoint.broadcast(
MagixMessage(
format = DATAFORGE_MAGIX_FORMAT,
id = "df[${payload.hashCode()}]",
origin = endpointID,
payload = payload
)
)
}.catch { error ->
logger.error(error) { "Error while sending a message" }
}.launchIn(this)
}

View File

@ -0,0 +1,62 @@
package space.kscience.controls.client
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import space.kscience.controls.api.DeviceMessage
import space.kscience.controls.manager.DeviceManager
import space.kscience.controls.manager.hubMessageFlow
import space.kscience.controls.manager.respondHubMessage
import space.kscience.dataforge.context.error
import space.kscience.dataforge.context.logger
import space.kscience.magix.api.*
public val controlsMagixFormat: MagixFormat<DeviceMessage> = MagixFormat(
DeviceMessage.serializer(),
setOf("controls-kt", "dataforge")
)
internal fun generateId(request: MagixMessage): String = if (request.id != null) {
"${request.id}.response"
} else {
"df[${request.payload.hashCode()}"
}
/**
* Communicate with server in [Magix format](https://github.com/waltz-controls/rfc/tree/master/1)
*/
public fun DeviceManager.connectToMagix(
endpoint: MagixEndpoint,
endpointID: String = controlsMagixFormat.defaultFormat,
): Job = context.launch {
endpoint.subscribe(controlsMagixFormat).onEach { (request, payload) ->
val responsePayload = respondHubMessage(payload)
if (responsePayload != null) {
endpoint.broadcast(
format = controlsMagixFormat,
origin = endpointID,
payload = responsePayload,
id = generateId(request),
parentId = request.id
)
}
}.catch { error ->
logger.error(error) { "Error while responding to message" }
}.launchIn(this)
hubMessageFlow(this).onEach { payload ->
endpoint.broadcast(
format = controlsMagixFormat,
origin = endpointID,
payload = payload,
id = "df[${payload.hashCode()}]"
)
}.catch { error ->
logger.error(error) { "Error while sending a message" }
}.launchIn(this)
}

View File

@ -1,4 +1,4 @@
package ru.mipt.npm.controls.client
package space.kscience.controls.client
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

View File

@ -1,18 +1,17 @@
package ru.mipt.npm.controls.client
package space.kscience.controls.client
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import kotlinx.serialization.Serializable
import ru.mipt.npm.controls.api.get
import ru.mipt.npm.controls.api.getOrReadProperty
import ru.mipt.npm.controls.controllers.DeviceManager
import ru.mipt.npm.magix.api.MagixEndpoint
import ru.mipt.npm.magix.api.MagixMessage
import space.kscience.controls.api.get
import space.kscience.controls.api.getOrReadProperty
import space.kscience.controls.manager.DeviceManager
import space.kscience.dataforge.context.error
import space.kscience.dataforge.context.logger
import space.kscience.dataforge.meta.Meta
import space.kscience.magix.api.*
public const val TANGO_MAGIX_FORMAT: String = "tango"
@ -59,33 +58,39 @@ public data class TangoPayload(
val argin: Meta? = null,
val argout: Meta? = null,
val data: Meta? = null,
val errors: List<String>? = null
val errors: List<String>? = null,
)
internal val tangoMagixFormat = MagixFormat(
TangoPayload.serializer(),
setOf("tango")
)
public fun DeviceManager.launchTangoMagix(
endpoint: MagixEndpoint<TangoPayload>,
endpoint: MagixEndpoint,
endpointID: String = TANGO_MAGIX_FORMAT,
): Job {
suspend fun respond(request: MagixMessage<TangoPayload>, payloadBuilder: (TangoPayload) -> TangoPayload) {
suspend fun respond(request: MagixMessage, payload: TangoPayload, payloadBuilder: (TangoPayload) -> TangoPayload) {
endpoint.broadcast(
request.copy(
id = generateId(request),
parentId = request.id,
origin = endpointID,
payload = payloadBuilder(request.payload)
)
tangoMagixFormat,
id = generateId(request),
parentId = request.id,
origin = endpointID,
payload = payloadBuilder(payload)
)
}
return context.launch {
endpoint.subscribe().onEach { request ->
endpoint.subscribe(tangoMagixFormat).onEach { (request, payload) ->
try {
val device = get(request.payload.device)
when (request.payload.action) {
val device = get(payload.device)
when (payload.action) {
TangoAction.read -> {
val value = device.getOrReadProperty(request.payload.name)
respond(request) { requestPayload ->
val value = device.getOrReadProperty(payload.name)
respond(request, payload) { requestPayload ->
requestPayload.copy(
value = value,
quality = TangoQuality.VALID
@ -93,12 +98,12 @@ public fun DeviceManager.launchTangoMagix(
}
}
TangoAction.write -> {
request.payload.value?.let { value ->
device.writeProperty(request.payload.name, value)
payload.value?.let { value ->
device.writeProperty(payload.name, value)
}
//wait for value to be written and return final state
val value = device.getOrReadProperty(request.payload.name)
respond(request) { requestPayload ->
val value = device.getOrReadProperty(payload.name)
respond(request, payload) { requestPayload ->
requestPayload.copy(
value = value,
quality = TangoQuality.VALID
@ -106,8 +111,8 @@ public fun DeviceManager.launchTangoMagix(
}
}
TangoAction.exec -> {
val result = device.execute(request.payload.name, request.payload.argin)
respond(request) { requestPayload ->
val result = device.execute(payload.name, payload.argin)
respond(request, payload) { requestPayload ->
requestPayload.copy(
argout = result,
quality = TangoQuality.VALID
@ -119,12 +124,11 @@ public fun DeviceManager.launchTangoMagix(
} catch (ex: Exception) {
logger.error(ex) { "Error while responding to message" }
endpoint.broadcast(
request.copy(
id = generateId(request),
parentId = request.id,
origin = endpointID,
payload = request.payload.copy(quality = TangoQuality.WARNING)
)
tangoMagixFormat,
id = generateId(request),
parentId = request.id,
origin = endpointID,
payload = payload.copy(quality = TangoQuality.WARNING)
)
}
}.launchIn(this)

View File

@ -1,14 +1,14 @@
plugins {
id("ru.mipt.npm.gradle.jvm")
id("space.kscience.gradle.jvm")
}
val ktorVersion: String by rootProject.extra
val miloVersion: String = "0.6.3"
val miloVersion: String = "0.6.7"
dependencies {
api(project(":controls-core"))
api("org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:${ru.mipt.npm.gradle.KScienceVersions.coroutinesVersion}")
api("org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:${space.kscience.gradle.KScienceVersions.coroutinesVersion}")
api("org.eclipse.milo:sdk-client:$miloVersion")
api("org.eclipse.milo:bsd-parser:$miloVersion")

View File

@ -1,4 +1,4 @@
package ru.mipt.npm.controls.opcua.client
package space.kscience.controls.opcua.client
import org.eclipse.milo.opcua.binaryschema.AbstractCodec
import org.eclipse.milo.opcua.binaryschema.parser.BsdParser
@ -11,12 +11,11 @@ import org.eclipse.milo.opcua.stack.core.types.builtin.*
import org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.*
import org.opcfoundation.opcua.binaryschema.EnumeratedType
import org.opcfoundation.opcua.binaryschema.StructuredType
import ru.mipt.npm.controls.misc.instant
import ru.mipt.npm.controls.misc.toMeta
import space.kscience.controls.misc.instant
import space.kscience.controls.misc.toMeta
import space.kscience.dataforge.meta.*
import space.kscience.dataforge.names.Name
import space.kscience.dataforge.names.asName
import space.kscience.dataforge.values.*
import java.util.*

View File

@ -1,11 +1,11 @@
package ru.mipt.npm.controls.opcua.client
package space.kscience.controls.opcua.client
import kotlinx.coroutines.future.await
import kotlinx.serialization.json.Json
import org.eclipse.milo.opcua.sdk.client.OpcUaClient
import org.eclipse.milo.opcua.stack.core.types.builtin.*
import org.eclipse.milo.opcua.stack.core.types.enumerated.TimestampsToReturn
import ru.mipt.npm.controls.api.Device
import space.kscience.controls.api.Device
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.MetaSerializer
import space.kscience.dataforge.meta.transformations.MetaConverter
@ -39,8 +39,8 @@ public suspend inline fun <reified T: Any> MiloDevice.readOpcWithTime(
val time = data.serverTime ?: error("No server time provided")
val meta: Meta = when (val content = data.value.value) {
is T -> return content to time
content is Meta -> content as Meta
content is ExtensionObject -> (content as ExtensionObject).decode(client.dynamicSerializationContext) as Meta
is Meta -> content
is ExtensionObject -> content.decode(client.dynamicSerializationContext) as Meta
else -> error("Incompatible OPC property value $content")
}

View File

@ -1,11 +1,11 @@
package ru.mipt.npm.controls.opcua.client
package space.kscience.controls.opcua.client
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import org.eclipse.milo.opcua.sdk.client.OpcUaClient
import org.eclipse.milo.opcua.stack.core.types.builtin.NodeId
import ru.mipt.npm.controls.properties.DeviceBySpec
import ru.mipt.npm.controls.properties.DeviceSpec
import space.kscience.controls.spec.DeviceBySpec
import space.kscience.controls.spec.DeviceSpec
import space.kscience.dataforge.context.Context
import space.kscience.dataforge.context.Global
import space.kscience.dataforge.meta.Meta

View File

@ -1,4 +1,4 @@
package ru.mipt.npm.controls.opcua.client
package space.kscience.controls.opcua.client
import org.eclipse.milo.opcua.sdk.client.OpcUaClient
import org.eclipse.milo.opcua.sdk.client.api.config.OpcUaClientConfigBuilder

View File

@ -1,4 +1,4 @@
package ru.mipt.npm.controls.opcua.server
package space.kscience.controls.opcua.server
import kotlinx.coroutines.launch
import kotlinx.datetime.toJavaInstant
@ -18,17 +18,17 @@ import org.eclipse.milo.opcua.stack.core.AttributeId
import org.eclipse.milo.opcua.stack.core.Identifiers
import org.eclipse.milo.opcua.stack.core.types.builtin.DateTime
import org.eclipse.milo.opcua.stack.core.types.builtin.LocalizedText
import ru.mipt.npm.controls.api.Device
import ru.mipt.npm.controls.api.DeviceHub
import ru.mipt.npm.controls.api.PropertyDescriptor
import ru.mipt.npm.controls.api.onPropertyChange
import ru.mipt.npm.controls.controllers.DeviceManager
import space.kscience.controls.api.Device
import space.kscience.controls.api.DeviceHub
import space.kscience.controls.api.PropertyDescriptor
import space.kscience.controls.api.onPropertyChange
import space.kscience.controls.manager.DeviceManager
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.MetaSerializer
import space.kscience.dataforge.meta.ValueType
import space.kscience.dataforge.names.Name
import space.kscience.dataforge.names.asName
import space.kscience.dataforge.names.plus
import space.kscience.dataforge.values.ValueType
public operator fun Device.get(propertyDescriptor: PropertyDescriptor): Meta? = getProperty(propertyDescriptor.name)
@ -208,5 +208,8 @@ public class DeviceNameSpace(
}
}
/**
* Serve devices from [deviceManager] as OPC-UA
*/
public fun OpcUaServer.serveDevices(deviceManager: DeviceManager): DeviceNameSpace =
DeviceNameSpace(this, deviceManager).apply { startup() }

View File

@ -1,14 +1,11 @@
package ru.mipt.npm.controls.opcua.server
package space.kscience.controls.opcua.server
import kotlinx.serialization.json.Json
import org.eclipse.milo.opcua.stack.core.types.builtin.DataValue
import org.eclipse.milo.opcua.stack.core.types.builtin.DateTime
import org.eclipse.milo.opcua.stack.core.types.builtin.StatusCode
import org.eclipse.milo.opcua.stack.core.types.builtin.Variant
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.MetaSerializer
import space.kscience.dataforge.meta.isLeaf
import space.kscience.dataforge.values.*
import space.kscience.dataforge.meta.*
import java.time.Instant
/**

View File

@ -1,4 +1,4 @@
package ru.mipt.npm.controls.opcua.server
package space.kscience.controls.opcua.server
import org.eclipse.milo.opcua.sdk.core.AccessLevel
import org.eclipse.milo.opcua.sdk.core.Reference

View File

@ -1,4 +1,4 @@
package ru.mipt.npm.controls.opcua.server
package space.kscience.controls.opcua.server
import org.eclipse.milo.opcua.sdk.server.OpcUaServer
import org.eclipse.milo.opcua.sdk.server.api.config.OpcUaServerConfig

View File

@ -1,5 +1,5 @@
plugins {
id("ru.mipt.npm.gradle.jvm")
id("space.kscience.gradle.jvm")
`maven-publish`
}

View File

@ -1,10 +1,10 @@
package ru.mipt.npm.controls.serial
package space.kscience.controls.serial
import jssc.SerialPort.*
import jssc.SerialPortEventListener
import ru.mipt.npm.controls.ports.AbstractPort
import ru.mipt.npm.controls.ports.Port
import ru.mipt.npm.controls.ports.PortFactory
import space.kscience.controls.ports.AbstractPort
import space.kscience.controls.ports.Port
import space.kscience.controls.ports.PortFactory
import space.kscience.dataforge.context.Context
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.int
@ -58,6 +58,9 @@ public class SerialPort private constructor(
public companion object : PortFactory {
override val type: String = "com"
/**
* Construct ComPort with given parameters
*/
@ -77,7 +80,7 @@ public class SerialPort private constructor(
return SerialPort(context, jssc, coroutineContext)
}
override fun invoke(meta: Meta, context: Context): Port {
override fun build(context: Context, meta: Meta): Port {
val name by meta.string { error("Serial port name not defined") }
val baudRate by meta.int(BAUDRATE_9600)
val dataBits by meta.int(DATABITS_8)

View File

@ -0,0 +1,31 @@
package space.kscience.controls.serial
import space.kscience.controls.ports.PortFactory
import space.kscience.dataforge.context.AbstractPlugin
import space.kscience.dataforge.context.Context
import space.kscience.dataforge.context.PluginFactory
import space.kscience.dataforge.context.PluginTag
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.names.Name
import kotlin.reflect.KClass
public class SerialPortPlugin : AbstractPlugin() {
override val tag: PluginTag get() = Companion.tag
override fun content(target: String): Map<Name, Any> = when(target){
PortFactory.TYPE -> mapOf(Name.EMPTY to SerialPort)
else -> emptyMap()
}
public companion object : PluginFactory<SerialPortPlugin> {
override val tag: PluginTag = PluginTag("controls.ports.serial", group = PluginTag.DATAFORGE_GROUP)
override val type: KClass<out SerialPortPlugin> = SerialPortPlugin::class
override fun build(context: Context, meta: Meta): SerialPortPlugin = SerialPortPlugin()
}
}

View File

@ -1,5 +1,5 @@
plugins {
id("ru.mipt.npm.gradle.jvm")
id("space.kscience.gradle.jvm")
`maven-publish`
}
@ -12,10 +12,12 @@ val ktorVersion: String by rootProject.extra
dependencies {
implementation(project(":controls-core"))
implementation(project(":controls-tcp"))
implementation(project(":controls-ktor-tcp"))
implementation(projects.magix.magixServer)
implementation("io.ktor:ktor-server-cio:$ktorVersion")
implementation("io.ktor:ktor-websockets:$ktorVersion")
implementation("io.ktor:ktor-serialization:$ktorVersion")
implementation("io.ktor:ktor-html-builder:$ktorVersion")
implementation("io.ktor:ktor-server-websockets:$ktorVersion")
implementation("io.ktor:ktor-server-content-negotiation:$ktorVersion")
implementation("io.ktor:ktor-serialization-kotlinx-json:$ktorVersion")
implementation("io.ktor:ktor-server-html-builder:$ktorVersion")
implementation("io.ktor:ktor-server-status-pages:$ktorVersion")
}

View File

@ -1,24 +1,23 @@
package ru.mipt.npm.controls.server
package space.kscience.controls.server
import io.ktor.application.*
import io.ktor.features.CORS
import io.ktor.features.StatusPages
import io.ktor.html.respondHtml
import io.ktor.http.HttpStatusCode
import io.ktor.request.receiveText
import io.ktor.response.respond
import io.ktor.response.respondRedirect
import io.ktor.response.respondText
import io.ktor.routing.get
import io.ktor.routing.post
import io.ktor.routing.route
import io.ktor.routing.routing
import io.ktor.server.application.*
import io.ktor.server.cio.CIO
import io.ktor.server.engine.ApplicationEngine
import io.ktor.server.engine.embeddedServer
import io.ktor.util.getValue
import io.ktor.websocket.WebSockets
import io.ktor.server.html.respondHtml
import io.ktor.server.plugins.statuspages.StatusPages
import io.ktor.server.request.receiveText
import io.ktor.server.response.respond
import io.ktor.server.response.respondRedirect
import io.ktor.server.response.respondText
import io.ktor.server.routing.get
import io.ktor.server.routing.post
import io.ktor.server.routing.route
import io.ktor.server.routing.routing
import io.ktor.server.util.getValue
import io.ktor.server.websocket.WebSockets
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.html.*
@ -27,19 +26,39 @@ import kotlinx.serialization.json.Json
import kotlinx.serialization.json.buildJsonArray
import kotlinx.serialization.json.encodeToJsonElement
import kotlinx.serialization.json.put
import ru.mipt.npm.controls.api.DeviceMessage
import ru.mipt.npm.controls.api.PropertyGetMessage
import ru.mipt.npm.controls.api.PropertySetMessage
import ru.mipt.npm.controls.api.getOrNull
import ru.mipt.npm.controls.controllers.DeviceManager
import ru.mipt.npm.controls.controllers.respondHubMessage
import ru.mipt.npm.magix.api.MagixEndpoint
import ru.mipt.npm.magix.server.GenericMagixMessage
import ru.mipt.npm.magix.server.launchMagixServerRawRSocket
import ru.mipt.npm.magix.server.magixModule
import space.kscience.controls.api.DeviceMessage
import space.kscience.controls.api.PropertyGetMessage
import space.kscience.controls.api.PropertySetMessage
import space.kscience.controls.api.getOrNull
import space.kscience.controls.manager.DeviceManager
import space.kscience.controls.manager.respondHubMessage
import space.kscience.dataforge.meta.toMeta
import space.kscience.dataforge.names.Name
import space.kscience.dataforge.names.asName
import space.kscience.magix.api.MagixEndpoint
import space.kscience.magix.api.MagixFlowPlugin
import space.kscience.magix.api.MagixMessage
import space.kscience.magix.server.magixModule
private fun Application.deviceServerModule(manager: DeviceManager) {
install(WebSockets)
// install(CORS) {
// anyHost()
// }
install(StatusPages) {
exception<IllegalArgumentException> { call, cause ->
call.respond(HttpStatusCode.BadRequest, cause.message ?: "")
}
}
deviceManagerModule(manager)
routing {
get("/") {
call.respondRedirect("/dashboard")
}
}
}
/**
* Create and start a web server for several devices
@ -48,26 +67,7 @@ public fun CoroutineScope.startDeviceServer(
manager: DeviceManager,
port: Int = MagixEndpoint.DEFAULT_MAGIX_HTTP_PORT,
host: String = "localhost",
): ApplicationEngine {
return this.embeddedServer(CIO, port, host) {
install(WebSockets)
install(CORS) {
anyHost()
}
install(StatusPages) {
exception<IllegalArgumentException> { cause ->
call.respond(HttpStatusCode.BadRequest, cause.message ?: "")
}
}
deviceManagerModule(manager)
routing {
get("/") {
call.respondRedirect("/dashboard")
}
}
}.start()
}
): ApplicationEngine = embeddedServer(CIO, port, host, module = { deviceServerModule(manager) }).start()
public fun ApplicationEngine.whenStarted(callback: Application.() -> Unit) {
environment.monitor.subscribe(ApplicationStarted, callback)
@ -78,20 +78,20 @@ public val WEB_SERVER_TARGET: Name = "@webServer".asName()
public fun Application.deviceManagerModule(
manager: DeviceManager,
vararg plugins: MagixFlowPlugin,
deviceNames: Collection<String> = manager.devices.keys.map { it.toString() },
route: String = "/",
rawSocketPort: Int = MagixEndpoint.DEFAULT_MAGIX_RAW_PORT,
buffer: Int = 100,
) {
if (featureOrNull(WebSockets) == null) {
if (pluginOrNull(WebSockets) == null) {
install(WebSockets)
}
if (featureOrNull(CORS) == null) {
install(CORS) {
anyHost()
}
}
// if (pluginOrNull(CORS) == null) {
// install(CORS) {
// anyHost()
// }
// }
routing {
route(route) {
@ -213,11 +213,13 @@ public fun Application.deviceManagerModule(
}
}
val magixFlow = MutableSharedFlow<GenericMagixMessage>(
val magixFlow = MutableSharedFlow<MagixMessage>(
buffer,
extraBufferCapacity = buffer
)
launchMagixServerRawRSocket(magixFlow, rawSocketPort)
plugins.forEach {
it.start(this, magixFlow)
}
magixModule(magixFlow)
}

View File

@ -1,12 +1,12 @@
package ru.mipt.npm.controls.server
package space.kscience.controls.server
import io.ktor.application.ApplicationCall
import io.ktor.http.ContentType
import io.ktor.response.respondText
import io.ktor.server.application.ApplicationCall
import io.ktor.server.response.respondText
import kotlinx.serialization.json.JsonObjectBuilder
import kotlinx.serialization.json.buildJsonObject
import ru.mipt.npm.controls.api.DeviceMessage
import ru.mipt.npm.magix.api.MagixEndpoint
import space.kscience.controls.api.DeviceMessage
import space.kscience.magix.api.MagixEndpoint
//internal fun Frame.toEnvelope(): Envelope {

View File

@ -0,0 +1,12 @@
# Description
This module provides API to store [DeviceMessages](/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/api/DeviceMessage.kt)
from certain [DeviceManager](/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/controllers/DeviceManager.kt)
or [MagixMessages](magix/magix-api/src/commonMain/kotlin/ru/mipt/npm/magix/api/MagixMessage.kt)
from certain [magix server](/magix/magix-server/src/main/kotlin/ru/mipt/npm/magix/server/server.kt).
# Usage
All usage examples can be found in [VirtualCarController](/demo/car/src/main/kotlin/ru/mipt/npm/controls/demo/car/VirtualCarController.kt).
For more details, you can see comments in source code of this module.

View File

@ -0,0 +1,23 @@
plugins {
id("space.kscience.gradle.mpp")
`maven-publish`
}
val dataforgeVersion: String by rootProject.extra
kscience{
jvm()
js()
dependencies {
api(projects.controlsCore)
}
dependencies(jvmMain){
api(projects.magix.magixApi)
api(projects.controlsMagixClient)
api(projects.magix.magixServer)
}
}
readme{
maturity = space.kscience.gradle.Maturity.PROTOTYPE
}

View File

@ -0,0 +1,19 @@
plugins {
id("space.kscience.gradle.jvm")
`maven-publish`
}
val xodusVersion: String by rootProject.extra
dependencies {
api(projects.controlsStorage)
implementation("org.jetbrains.xodus:xodus-entity-store:$xodusVersion")
// implementation("org.jetbrains.xodus:xodus-environment:$xodusVersion")
// implementation("org.jetbrains.xodus:xodus-vfs:$xodusVersion")
testImplementation(npmlibs.kotlinx.coroutines.test)
}
readme{
maturity = space.kscience.gradle.Maturity.PROTOTYPE
}

View File

@ -0,0 +1,139 @@
package space.kscience.controls.xodus
import jetbrains.exodus.entitystore.Entity
import jetbrains.exodus.entitystore.PersistentEntityStore
import jetbrains.exodus.entitystore.PersistentEntityStores
import jetbrains.exodus.entitystore.StoreTransaction
import kotlinx.datetime.Instant
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.descriptors.serialDescriptor
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import space.kscience.controls.api.DeviceMessage
import space.kscience.controls.storage.DeviceMessageStorage
import space.kscience.controls.storage.workDirectory
import space.kscience.dataforge.context.Context
import space.kscience.dataforge.context.Factory
import space.kscience.dataforge.context.fetch
import space.kscience.dataforge.io.IOPlugin
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.get
import space.kscience.dataforge.meta.string
import space.kscience.dataforge.misc.DFExperimental
import space.kscience.dataforge.names.Name
import space.kscience.dataforge.names.matches
import space.kscience.dataforge.names.parseAsName
internal fun StoreTransaction.writeMessage(message: DeviceMessage): Unit {
val entity: Entity = newEntity(XodusDeviceMessageStorage.DEVICE_MESSAGE_ENTITY_TYPE)
val json = Json.encodeToJsonElement(DeviceMessage.serializer(), message).jsonObject
val type = json["type"]?.jsonPrimitive?.content ?: error("Message json representation must have type.")
entity.setProperty("type", type)
message.sourceDevice?.let {
entity.setProperty(DeviceMessage::sourceDevice.name, it.toString())
}
message.targetDevice?.let {
entity.setProperty(DeviceMessage::targetDevice.name, it.toString())
}
message.time?.let {
entity.setProperty(DeviceMessage::targetDevice.name, it.toString())
}
entity.setBlobString("json", Json.encodeToString(json))
}
@OptIn(DFExperimental::class)
private fun Entity.propertyMatchesName(propertyName: String, pattern: Name? = null) =
pattern == null || getProperty(propertyName).toString().parseAsName().matches(pattern)
private fun Entity.timeInRange(range: ClosedRange<Instant>?): Boolean {
if (range == null) return true
val time: Instant? = getProperty(DeviceMessage::time.name)?.let { entityString ->
Instant.parse(entityString.toString())
}
return time != null && time in range
}
public class XodusDeviceMessageStorage(
private val entityStore: PersistentEntityStore,
) : DeviceMessageStorage, AutoCloseable {
override suspend fun write(event: DeviceMessage) {
entityStore.executeInTransaction { txn ->
txn.writeMessage(event)
}
}
override suspend fun readAll(): List<DeviceMessage> = entityStore.computeInReadonlyTransaction { transaction ->
transaction.sort(
DEVICE_MESSAGE_ENTITY_TYPE,
DeviceMessage::time.name,
true
).map {
Json.decodeFromString(
DeviceMessage.serializer(),
it.getBlobString("json") ?: error("No json content found")
)
}
}
override suspend fun read(
eventType: String,
range: ClosedRange<Instant>?,
sourceDevice: Name?,
targetDevice: Name?,
): List<DeviceMessage> = entityStore.computeInReadonlyTransaction { transaction ->
transaction.find(
DEVICE_MESSAGE_ENTITY_TYPE,
"type",
eventType
).asSequence().filter {
it.timeInRange(range) &&
it.propertyMatchesName(DeviceMessage::sourceDevice.name, sourceDevice) &&
it.propertyMatchesName(DeviceMessage::targetDevice.name, targetDevice)
}.map {
Json.decodeFromString(
DeviceMessage.serializer(),
it.getBlobString("json") ?: error("No json content found")
)
}.sortedBy { it.time }.toList()
}
override fun close() {
entityStore.close()
}
public companion object : Factory<XodusDeviceMessageStorage> {
internal const val DEVICE_MESSAGE_ENTITY_TYPE = "controls-kt.message"
public val XODUS_STORE_PROPERTY: Name = Name.of("xodus", "storagePath")
override fun build(context: Context, meta: Meta): XodusDeviceMessageStorage {
val io = context.fetch(IOPlugin)
val storePath = io.workDirectory.resolve(
meta[XODUS_STORE_PROPERTY]?.string
?: context.properties[XODUS_STORE_PROPERTY]?.string ?: "storage"
)
val entityStore = PersistentEntityStores.newInstance(storePath.toFile())
return XodusDeviceMessageStorage(entityStore)
}
}
}
/**
* Query all messages of given type
*/
@OptIn(ExperimentalSerializationApi::class)
public suspend inline fun <reified T : DeviceMessage> XodusDeviceMessageStorage.query(
range: ClosedRange<Instant>? = null,
sourceDevice: Name? = null,
targetDevice: Name? = null,
): List<T> = read(serialDescriptor<T>().serialName, range, sourceDevice, targetDevice).map {
//Check that all types are correct
it as T
}

View File

@ -0,0 +1,77 @@
import jetbrains.exodus.entitystore.PersistentEntityStores
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import kotlinx.datetime.Instant
import org.junit.jupiter.api.AfterAll
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.BeforeAll
import org.junit.jupiter.api.Test
import space.kscience.controls.api.PropertyChangedMessage
import space.kscience.controls.xodus.XodusDeviceMessageStorage
import space.kscience.controls.xodus.query
import space.kscience.controls.xodus.writeMessage
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.names.Name
import space.kscience.dataforge.names.asName
import java.nio.file.Files
internal class PropertyHistoryTest {
companion object {
val storeFile = Files.createTempDirectory("controls-xodus").toFile()
private val propertyChangedMessages = listOf(
PropertyChangedMessage(
"speed",
Meta.EMPTY,
time = Instant.fromEpochMilliseconds(1000),
sourceDevice = Name.of("virtual-car")
),
PropertyChangedMessage(
"acceleration",
Meta.EMPTY,
time = Instant.fromEpochMilliseconds(1500),
sourceDevice = Name.of("virtual-car")
),
PropertyChangedMessage(
"speed",
Meta.EMPTY,
time = Instant.fromEpochMilliseconds(2000),
sourceDevice = Name.of("magix-virtual-car")
)
)
@BeforeAll
@JvmStatic
fun createEntities() {
PersistentEntityStores.newInstance(storeFile).use {
it.executeInTransaction { transaction ->
propertyChangedMessages.forEach { message ->
transaction.writeMessage(message)
}
}
}
}
@AfterAll
@JvmStatic
fun deleteDatabase() {
storeFile.deleteRecursively()
}
}
@OptIn(ExperimentalCoroutinesApi::class)
@Test
fun getPropertyHistoryTest() = runTest {
PersistentEntityStores.newInstance(storeFile).use { entityStore ->
XodusDeviceMessageStorage(entityStore).use { storage ->
assertEquals(
propertyChangedMessages[0],
storage.query<PropertyChangedMessage>(
sourceDevice = "virtual-car".asName()
).first { it.property == "speed" }
)
}
}
}
}

View File

@ -0,0 +1,23 @@
package space.kscience.controls.storage
import kotlinx.datetime.Instant
import space.kscience.controls.api.DeviceMessage
import space.kscience.dataforge.names.Name
/**
* A storage for Controls-kt [DeviceMessage]
*/
public interface DeviceMessageStorage {
public suspend fun write(event: DeviceMessage)
public suspend fun readAll(): List<DeviceMessage>
public suspend fun read(
eventType: String,
range: ClosedRange<Instant>? = null,
sourceDevice: Name? = null,
targetDevice: Name? = null,
): List<DeviceMessage>
public fun close()
}

View File

@ -0,0 +1,64 @@
package space.kscience.controls.storage
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onCompletion
import kotlinx.coroutines.flow.onEach
import space.kscience.controls.api.DeviceMessage
import space.kscience.controls.manager.DeviceManager
import space.kscience.controls.manager.hubMessageFlow
import space.kscience.dataforge.context.Factory
import space.kscience.dataforge.context.debug
import space.kscience.dataforge.context.logger
//TODO replace by plugin?
public fun DeviceManager.storage(
factory: Factory<DeviceMessageStorage>,
): DeviceMessageStorage = factory.build(context, meta)
/**
* Begin to store DeviceMessages from this DeviceManager
* @param factory factory that will be used for creating persistent entity store instance. DefaultPersistentStoreFactory by default.
* DeviceManager's meta and context will be used for in invoke method.
* @param filterCondition allow you to specify messages which we want to store. Always true by default.
* @return Job which responsible for our storage
*/
public fun DeviceManager.storeMessages(
factory: Factory<DeviceMessageStorage>,
filterCondition: suspend (DeviceMessage) -> Boolean = { true },
): Job {
val storage = factory.build(context, meta)
logger.debug { "Message storage with meta = $meta created" }
return hubMessageFlow(context).filter(filterCondition).onEach { message ->
storage.write(message)
}.onCompletion {
storage.close()
logger.debug { "Message storage closed" }
}.launchIn(context)
}
///**
// * @return the list of deviceMessages that describes changes of specified property of specified device sorted by time
// * @param sourceDeviceName a name of device, history of which property we want to get
// * @param propertyName a name of property, history of which we want to get
// * @param factory a factory that produce mongo clients
// */
//public suspend fun getPropertyHistory(
// sourceDeviceName: String,
// propertyName: String,
// factory: Factory<EventStorage>,
// meta: Meta = Meta.EMPTY,
//): List<PropertyChangedMessage> {
// return factory(meta).use {
// it.getPropertyHistory(sourceDeviceName, propertyName)
// }
//}
//
//
//public enum class StorageKind {
// DEVICE_HUB,
// MAGIX_SERVER
//}

View File

@ -0,0 +1,46 @@
//package space.kscience.controls.storage
//
//import io.ktor.server.application.Application
//import kotlinx.coroutines.InternalCoroutinesApi
//import kotlinx.coroutines.flow.Flow
//import kotlinx.coroutines.flow.MutableSharedFlow
//import kotlinx.coroutines.flow.filter
//import kotlinx.coroutines.flow.onEach
//import kotlinx.coroutines.job
//import ru.mipt.npm.magix.server.GenericMagixMessage
//import space.kscience.dataforge.context.Factory
//import space.kscience.dataforge.meta.Meta
//
///**
// * Asynchronous version of synchronous API, so for more details check relative docs
// */
//
//internal fun Flow<GenericMagixMessage>.store(
// client: EventStorage,
// flowFilter: suspend (GenericMagixMessage) -> Boolean = { true },
//) {
// filter(flowFilter).onEach { message ->
// client.storeMagixMessage(message)
// }
//}
//
///** Begin to store MagixMessages from certain flow
// * @param flow flow of messages which we will store
// * @param meta Meta which may have some configuration parameters for our storage and will be used in invoke method of factory
// * @param factory factory that will be used for creating persistent entity store instance. DefaultPersistentStoreFactory by default.
// * @param flowFilter allow you to specify messages which we want to store. Always true by default.
// */
//@OptIn(InternalCoroutinesApi::class)
//public fun Application.store(
// flow: MutableSharedFlow<GenericMagixMessage>,
// factory: Factory<EventStorage>,
// meta: Meta = Meta.EMPTY,
// flowFilter: suspend (GenericMagixMessage) -> Boolean = { true },
//) {
// val client = factory(meta)
//
// flow.store(client, flowFilter)
// coroutineContext.job.invokeOnCompletion(onCancelling = true) {
// client.close()
// }
//}

View File

@ -0,0 +1,32 @@
package space.kscience.controls.storage
import space.kscience.dataforge.context.ContextBuilder
import space.kscience.dataforge.io.IOPlugin
import space.kscience.dataforge.meta.get
import space.kscience.dataforge.meta.set
import space.kscience.dataforge.meta.string
import java.nio.file.Path
import kotlin.io.path.Path
//TODO remove on DF 0.6
internal val IOPlugin.Companion.WORK_DIRECTORY_KEY: String get() = ".dataforge"
public val IOPlugin.workDirectory: Path
get() {
val workDirectoryPath = meta[IOPlugin.WORK_DIRECTORY_KEY].string
?: context.properties[IOPlugin.WORK_DIRECTORY_KEY].string
?: ".dataforge"
return Path(workDirectoryPath)
}
public fun ContextBuilder.workDirectory(path: String) {
properties {
set(IOPlugin.WORK_DIRECTORY_KEY, path)
}
}
public fun ContextBuilder.workDirectory(path: Path){
workDirectory(path.toAbsolutePath().toString())
}

View File

@ -1,17 +0,0 @@
plugins {
id("ru.mipt.npm.gradle.mpp")
}
val ktorVersion: String by rootProject.extra
kotlin {
sourceSets {
commonMain {
dependencies {
api(project(":controls-core"))
api("io.ktor:ktor-network:$ktorVersion")
}
}
}
}

View File

@ -1,21 +1,19 @@
plugins {
kotlin("jvm")
id("org.openjfx.javafxplugin") version "0.0.9"
id("org.openjfx.javafxplugin") version "0.0.13"
application
}
repositories{
repositories {
mavenCentral()
jcenter()
maven("https://repo.kotlin.link")
maven("https://kotlin.bintray.com/kotlinx")
}
val ktorVersion: String by rootProject.extra
val rsocketVersion: String by rootProject.extra
dependencies{
dependencies {
implementation(projects.controlsCore)
//implementation(projects.controlsServer)
implementation(projects.magix.magixServer)
@ -26,23 +24,23 @@ dependencies{
implementation("io.ktor:ktor-client-cio:$ktorVersion")
implementation("no.tornado:tornadofx:1.7.20")
implementation("space.kscience:plotlykt-server:0.5.0-dev-1")
implementation("com.github.Ricky12Awesome:json-schema-serialization:0.6.6")
implementation("ch.qos.logback:logback-classic:1.2.3")
implementation("space.kscience:plotlykt-server:0.5.3-dev-1")
// implementation("com.github.Ricky12Awesome:json-schema-serialization:0.6.6")
implementation("ch.qos.logback:logback-classic:1.2.11")
}
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach {
kotlinOptions {
jvmTarget = "11"
freeCompilerArgs = freeCompilerArgs + "-Xjvm-default=all"
freeCompilerArgs = freeCompilerArgs + listOf("-Xjvm-default=all", "-Xopt-in=kotlin.RequiresOptIn")
}
}
javafx{
version = "14"
javafx {
version = "17"
modules("javafx.controls")
}
application{
mainClass.set("ru.mipt.npm.controls.demo.DemoControllerViewKt")
application {
mainClass.set("space.kscience.controls.demo.DemoControllerViewKt")
}

View File

@ -1,4 +1,4 @@
package ru.mipt.npm.controls.demo
package space.kscience.controls.demo
import io.ktor.server.engine.ApplicationEngine
import javafx.scene.Parent
@ -8,21 +8,22 @@ import javafx.stage.Stage
import kotlinx.coroutines.launch
import org.eclipse.milo.opcua.sdk.server.OpcUaServer
import org.eclipse.milo.opcua.stack.core.types.builtin.LocalizedText
import ru.mipt.npm.controls.api.DeviceMessage
import ru.mipt.npm.controls.client.connectToMagix
import ru.mipt.npm.controls.controllers.DeviceManager
import ru.mipt.npm.controls.controllers.install
import ru.mipt.npm.controls.demo.DemoDevice.Companion.cosScale
import ru.mipt.npm.controls.demo.DemoDevice.Companion.sinScale
import ru.mipt.npm.controls.demo.DemoDevice.Companion.timeScale
import ru.mipt.npm.controls.opcua.server.OpcUaServer
import ru.mipt.npm.controls.opcua.server.endpoint
import ru.mipt.npm.controls.opcua.server.serveDevices
import ru.mipt.npm.magix.api.MagixEndpoint
import ru.mipt.npm.magix.rsocket.rSocketWithTcp
import ru.mipt.npm.magix.rsocket.rSocketWithWebSockets
import ru.mipt.npm.magix.server.startMagixServer
import space.kscience.controls.client.connectToMagix
import space.kscience.controls.demo.DemoDevice.Companion.cosScale
import space.kscience.controls.demo.DemoDevice.Companion.sinScale
import space.kscience.controls.demo.DemoDevice.Companion.timeScale
import space.kscience.controls.manager.DeviceManager
import space.kscience.controls.manager.install
import space.kscience.controls.opcua.server.OpcUaServer
import space.kscience.controls.opcua.server.endpoint
import space.kscience.controls.opcua.server.serveDevices
import space.kscience.dataforge.context.*
import space.kscience.magix.api.MagixEndpoint
import space.kscience.magix.rsocket.rSocketWithTcp
import space.kscience.magix.rsocket.rSocketWithWebSockets
import space.kscience.magix.server.RSocketMagixFlowPlugin
import space.kscience.magix.server.startMagixServer
import space.kscince.magix.zmq.ZmqMagixFlowPlugin
import tornadofx.*
import java.awt.Desktop
import java.net.URI
@ -33,7 +34,7 @@ class DemoController : Controller(), ContextAware {
var magixServer: ApplicationEngine? = null
var visualizer: ApplicationEngine? = null
var opcUaServer: OpcUaServer = OpcUaServer {
setApplicationName(LocalizedText.english("ru.mipt.npm.controls.opcua"))
setApplicationName(LocalizedText.english("space.kscience.controls.opcua"))
endpoint {
setBindPort(9999)
//use default endpoint
@ -44,19 +45,24 @@ class DemoController : Controller(), ContextAware {
plugin(DeviceManager)
}
private val deviceManager = context.fetch(DeviceManager)
private val deviceManager = context.request(DeviceManager)
fun init() {
context.launch {
device = deviceManager.install("demo", DemoDevice)
//starting magix event loop
magixServer = startMagixServer(enableRawRSocket = true, enableZmq = true)
magixServer = startMagixServer(
RSocketMagixFlowPlugin(), //TCP rsocket support
ZmqMagixFlowPlugin() //ZMQ support
)
//Launch device client and connect it to the server
val deviceEndpoint = MagixEndpoint.rSocketWithTcp("localhost", DeviceMessage.serializer())
val deviceEndpoint = MagixEndpoint.rSocketWithTcp("localhost")
deviceManager.connectToMagix(deviceEndpoint)
val visualEndpoint = MagixEndpoint.rSocketWithWebSockets("localhost", DeviceMessage.serializer())
//connect visualization to a magix endpoint
val visualEndpoint = MagixEndpoint.rSocketWithWebSockets("localhost")
visualizer = visualEndpoint.startDemoDeviceServer()
//serve devices as OPC-UA namespace
opcUaServer.startup()
opcUaServer.serveDevices(deviceManager)
}

View File

@ -0,0 +1,84 @@
package space.kscience.controls.demo
import kotlinx.coroutines.launch
import space.kscience.controls.api.metaDescriptor
import space.kscience.controls.spec.*
import space.kscience.dataforge.context.Context
import space.kscience.dataforge.context.Factory
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.ValueType
import space.kscience.dataforge.meta.descriptors.value
import space.kscience.dataforge.meta.transformations.MetaConverter
import java.time.Instant
import kotlin.time.Duration.Companion.milliseconds
class DemoDevice(context: Context, meta: Meta) : DeviceBySpec<DemoDevice>(DemoDevice, context, meta) {
private var timeScaleState = 5000.0
private var sinScaleState = 1.0
private var cosScaleState = 1.0
companion object : DeviceSpec<DemoDevice>(), Factory<DemoDevice> {
override fun build(context: Context, meta: Meta): DemoDevice = DemoDevice(context, meta)
// register virtual properties based on actual object state
val timeScale by mutableProperty(MetaConverter.double, DemoDevice::timeScaleState) {
metaDescriptor {
type(ValueType.NUMBER)
}
info = "Real to virtual time scale"
}
val sinScale by mutableProperty(MetaConverter.double, DemoDevice::sinScaleState)
val cosScale by mutableProperty(MetaConverter.double, DemoDevice::cosScaleState)
val sin by doubleProperty {
val time = Instant.now()
kotlin.math.sin(time.toEpochMilli().toDouble() / timeScaleState) * sinScaleState
}
val cos by doubleProperty {
val time = Instant.now()
kotlin.math.cos(time.toEpochMilli().toDouble() / timeScaleState) * sinScaleState
}
val coordinates by metaProperty(
descriptorBuilder = {
metaDescriptor {
value("time", ValueType.NUMBER)
}
}
) {
Meta {
val time = Instant.now()
"time" put time.toEpochMilli()
"x" put read(sin)
"y" put read(cos)
}
}
override suspend fun DemoDevice.onOpen() {
launch {
sinScale.read()
cosScale.read()
timeScale.read()
}
doRecurring(50.milliseconds) {
sin.read()
cos.read()
coordinates.read()
}
}
val resetScale by action(MetaConverter.meta, MetaConverter.meta) {
timeScale.write(5000.0)
sinScale.write(1.0)
cosScale.write(1.0)
null
}
}
}

View File

@ -1,21 +1,22 @@
package ru.mipt.npm.controls.demo
package space.kscience.controls.demo
import io.ktor.application.install
import io.ktor.features.CORS
import io.ktor.server.application.install
import io.ktor.server.cio.CIO
import io.ktor.server.engine.ApplicationEngine
import io.ktor.server.engine.embeddedServer
import io.ktor.websocket.WebSockets
import io.rsocket.kotlin.transport.ktor.server.RSocketSupport
import io.ktor.server.plugins.cors.routing.CORS
import io.ktor.server.websocket.WebSockets
import io.rsocket.kotlin.ktor.server.RSocketSupport
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import kotlinx.html.div
import kotlinx.html.link
import ru.mipt.npm.controls.api.DeviceMessage
import ru.mipt.npm.controls.api.PropertyChangedMessage
import ru.mipt.npm.magix.api.MagixEndpoint
import space.kscience.controls.api.PropertyChangedMessage
import space.kscience.controls.client.controlsMagixFormat
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.double
import space.kscience.magix.api.MagixEndpoint
import space.kscience.magix.api.subscribe
import space.kscience.plotly.layout
import space.kscience.plotly.models.Trace
import space.kscience.plotly.plot
@ -54,33 +55,33 @@ suspend fun Trace.updateXYFrom(flow: Flow<Iterable<Pair<Double, Double>>>) {
}
suspend fun MagixEndpoint<DeviceMessage>.startDemoDeviceServer(): ApplicationEngine =
embeddedServer(CIO, 9090) {
install(WebSockets)
install(RSocketSupport)
@Suppress("ExtractKtorModule")
suspend fun MagixEndpoint.startDemoDeviceServer(): ApplicationEngine = embeddedServer(CIO, 9091) {
install(WebSockets)
install(RSocketSupport)
install(CORS) {
anyHost()
}
install(CORS) {
anyHost()
}
val sinFlow = MutableSharedFlow<Meta?>()// = device.sin.flow()
val cosFlow = MutableSharedFlow<Meta?>()// = device.cos.flow()
val sinFlow = MutableSharedFlow<Meta?>()// = device.sin.flow()
val cosFlow = MutableSharedFlow<Meta?>()// = device.cos.flow()
launch {
subscribe().collect { magix ->
(magix.payload as? PropertyChangedMessage)?.let { message ->
when (message.property) {
"sin" -> sinFlow.emit(message.value)
"cos" -> cosFlow.emit(message.value)
}
launch {
subscribe(controlsMagixFormat).collect { (_, payload) ->
(payload as? PropertyChangedMessage)?.let { message ->
when (message.property) {
"sin" -> sinFlow.emit(message.value)
"cos" -> cosFlow.emit(message.value)
}
}
}
}
plotlyModule().apply {
updateMode = PlotlyUpdateMode.PUSH
updateInterval = 50
}.page { container ->
plotlyModule{
updateMode = PlotlyUpdateMode.PUSH
updateInterval = 50
page { container ->
val sinCosFlow = sinFlow.zip(cosFlow) { sin, cos ->
sin.double!! to cos.double!!
}
@ -140,6 +141,8 @@ suspend fun MagixEndpoint<DeviceMessage>.startDemoDeviceServer(): ApplicationEng
}
}
}
}
}.apply { start() }
}
}
}.apply { start() }

View File

@ -0,0 +1,10 @@
package space.kscience.controls.demo
//import com.github.ricky12awesome.jss.encodeToSchema
//import com.github.ricky12awesome.jss.globalJson
//import space.kscience.controls.api.DeviceMessage
//fun main() {
// val schema = globalJson.encodeToSchema(DeviceMessage.serializer(), generateDefinitions = false)
// println(schema)
//}

52
demo/car/build.gradle.kts Normal file
View File

@ -0,0 +1,52 @@
plugins {
kotlin("jvm")
id("org.openjfx.javafxplugin") version "0.0.10"
application
}
repositories {
mavenCentral()
maven("https://repo.kotlin.link")
}
val ktorVersion: String by rootProject.extra
val rsocketVersion: String by rootProject.extra
dependencies {
implementation(projects.controlsCore)
implementation(projects.magix.magixApi)
implementation(projects.magix.magixServer)
implementation(projects.magix.magixRsocket)
implementation(projects.magix.magixZmq)
implementation(projects.controlsMagixClient)
implementation(projects.controlsStorage.controlsXodus)
implementation(projects.magix.magixStorage.magixStorageXodus)
// implementation(projects.controlsMongo)
implementation("io.ktor:ktor-client-cio:$ktorVersion")
implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.3.1")
implementation("no.tornado:tornadofx:1.7.20")
implementation("space.kscience:plotlykt-server:0.5.0")
implementation("ch.qos.logback:logback-classic:1.2.11")
implementation("org.jetbrains.xodus:xodus-entity-store:1.3.232")
implementation("org.jetbrains.xodus:xodus-environment:1.3.232")
implementation("org.jetbrains.xodus:xodus-vfs:1.3.232")
// implementation("org.litote.kmongo:kmongo-coroutine-serialization:4.4.0")
}
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach {
kotlinOptions {
jvmTarget = "11"
freeCompilerArgs = freeCompilerArgs + listOf("-Xjvm-default=all", "-Xopt-in=kotlin.RequiresOptIn")
}
}
javafx {
version = "14"
modules("javafx.controls")
}
application {
mainClass.set("space.kscience.controls.demo.car.VirtualCarControllerKt")
}

View File

@ -0,0 +1,27 @@
package space.kscience.controls.demo.car
import space.kscience.controls.api.Device
import space.kscience.controls.spec.DeviceSpec
interface IVirtualCar : Device {
var speedState: Vector2D
var locationState: Vector2D
var accelerationState: Vector2D
companion object : DeviceSpec<IVirtualCar>() {
/**
* Read-only speed
*/
val speed by property(Vector2D, IVirtualCar::speedState)
/**
* Read-only location
*/
val location by property(Vector2D, IVirtualCar::locationState)
/**
* writable acceleration
*/
val acceleration by mutableProperty(Vector2D, IVirtualCar::accelerationState)
}
}

View File

@ -0,0 +1,48 @@
package space.kscience.controls.demo.car
import kotlinx.coroutines.launch
import space.kscience.controls.api.PropertyChangedMessage
import space.kscience.controls.client.controlsMagixFormat
import space.kscience.dataforge.context.Context
import space.kscience.dataforge.context.Factory
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.get
import space.kscience.dataforge.meta.string
import space.kscience.dataforge.names.Name
import space.kscience.magix.api.MagixEndpoint
import space.kscience.magix.api.subscribe
import space.kscience.magix.rsocket.rSocketWithWebSockets
import kotlin.time.ExperimentalTime
class MagixVirtualCar(context: Context, meta: Meta) : VirtualCar(context, meta) {
private fun MagixEndpoint.launchMagixVirtualCarUpdate() = launch {
subscribe(controlsMagixFormat).collect { (_, payload) ->
(payload as? PropertyChangedMessage)?.let { message ->
if (message.sourceDevice == Name.parse("virtual-car")) {
when (message.property) {
"acceleration" -> IVirtualCar.acceleration.write(Vector2D.metaToObject(message.value))
}
}
}
}
}
@OptIn(ExperimentalTime::class)
override suspend fun open() {
super.open()
val magixEndpoint = MagixEndpoint.rSocketWithWebSockets(
meta["magixServerHost"].string ?: "localhost",
)
launch {
magixEndpoint.launchMagixVirtualCarUpdate()
}
}
companion object : Factory<MagixVirtualCar> {
override fun build(context: Context, meta: Meta): MagixVirtualCar = MagixVirtualCar(context, meta)
}
}

View File

@ -0,0 +1,115 @@
@file:OptIn(ExperimentalTime::class)
package space.kscience.controls.demo.car
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.datetime.Clock
import kotlinx.datetime.Instant
import space.kscience.controls.spec.DeviceBySpec
import space.kscience.controls.spec.doRecurring
import space.kscience.dataforge.context.Context
import space.kscience.dataforge.context.Factory
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.MetaRepr
import space.kscience.dataforge.meta.double
import space.kscience.dataforge.meta.get
import space.kscience.dataforge.meta.transformations.MetaConverter
import kotlin.math.pow
import kotlin.time.Duration
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.ExperimentalTime
data class Vector2D(var x: Double = 0.0, var y: Double = 0.0) : MetaRepr {
override fun toMeta(): Meta = objectToMeta(this)
operator fun div(arg: Double): Vector2D = Vector2D(x / arg, y / arg)
companion object CoordinatesMetaConverter : MetaConverter<Vector2D> {
override fun metaToObject(meta: Meta): Vector2D = Vector2D(
meta["x"].double ?: 0.0,
meta["y"].double ?: 0.0
)
override fun objectToMeta(obj: Vector2D): Meta = Meta {
"x" put obj.x
"y" put obj.y
}
}
}
open class VirtualCar(context: Context, meta: Meta) : DeviceBySpec<VirtualCar>(IVirtualCar, context, meta), IVirtualCar {
private val timeScale = 1e-3
private val mass by meta.double(1000.0) // mass in kilograms
override var speedState: Vector2D = Vector2D()
override var locationState: Vector2D = Vector2D()
override var accelerationState: Vector2D = Vector2D()
set(value) {
update()
field = value
}
private var timeState: Instant? = null
private fun update(newTime: Instant = Clock.System.now()) {
//initialize time if it is not initialized
if (timeState == null) {
timeState = newTime
return
}
val dt: Double = (newTime - (timeState ?: return)).inWholeMilliseconds.toDouble() * timeScale
locationState.apply {
x += speedState.x * dt + accelerationState.x * dt.pow(2) / 2.0
y += speedState.y * dt + accelerationState.y * dt.pow(2) / 2.0
}
speedState.apply {
x += dt * accelerationState.x
y += dt * accelerationState.y
}
//TODO apply friction. One can introduce rotation of the cabin and different friction coefficients along the axis
launch {
//update logical states
IVirtualCar.location.read()
IVirtualCar.speed.read()
IVirtualCar.acceleration.read()
}
}
fun applyForce(force: Vector2D, duration: Duration) {
launch {
update()
accelerationState = force / mass
delay(duration)
accelerationState.apply {
x = 0.0
y = 0.0
}
update()
}
}
@OptIn(ExperimentalTime::class)
override suspend fun open() {
super<DeviceBySpec>.open()
//initializing the clock
timeState = Clock.System.now()
//starting regular updates
doRecurring(100.milliseconds) {
update()
}
}
companion object : Factory<VirtualCar> {
override fun build(context: Context, meta: Meta): VirtualCar = VirtualCar(context, meta)
}
}

View File

@ -0,0 +1,146 @@
package space.kscience.controls.demo.car
import io.ktor.server.engine.ApplicationEngine
import javafx.beans.property.DoubleProperty
import javafx.scene.Parent
import javafx.scene.control.TextField
import javafx.scene.layout.Priority
import javafx.stage.Stage
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import space.kscience.controls.client.connectToMagix
import space.kscience.controls.demo.car.IVirtualCar.Companion.acceleration
import space.kscience.controls.manager.DeviceManager
import space.kscience.controls.manager.install
import space.kscience.controls.storage.storeMessages
import space.kscience.controls.xodus.XodusDeviceMessageStorage
import space.kscience.dataforge.context.*
import space.kscience.dataforge.meta.Meta
import space.kscience.magix.api.MagixEndpoint
import space.kscience.magix.rsocket.rSocketWithTcp
import space.kscience.magix.server.RSocketMagixFlowPlugin
import space.kscience.magix.server.startMagixServer
import space.kscience.magix.storage.xodus.storeInXodus
import space.kscince.magix.zmq.ZmqMagixFlowPlugin
import tornadofx.*
import java.nio.file.Paths
class VirtualCarController : Controller(), ContextAware {
var virtualCar: VirtualCar? = null
var magixVirtualCar: MagixVirtualCar? = null
var magixServer: ApplicationEngine? = null
var xodusStorageJob: Job? = null
var storageEndpoint: MagixEndpoint? = null
//var mongoStorageJob: Job? = null
override val context = Context("demoDevice") {
plugin(DeviceManager)
}
private val deviceManager = context.fetch(DeviceManager, Meta {
"xodusConfig" put {
"entityStorePath" put deviceEntityStorePath.toString()
}
})
fun init() {
context.launch {
virtualCar = deviceManager.install("virtual-car", VirtualCar)
//starting magix event loop and connect it to entity store
magixServer = startMagixServer(RSocketMagixFlowPlugin(), ZmqMagixFlowPlugin())
storageEndpoint = MagixEndpoint.rSocketWithTcp("localhost").apply {
storeInXodus(this@launch, magixEntityStorePath)
}
magixVirtualCar = deviceManager.install("magix-virtual-car", MagixVirtualCar)
//connect to device entity store
xodusStorageJob = deviceManager.storeMessages(XodusDeviceMessageStorage)
//Create mongo client and connect to MongoDB
//mongoStorageJob = deviceManager.storeMessages(DefaultAsynchronousMongoClientFactory)
//Launch device client and connect it to the server
val deviceEndpoint = MagixEndpoint.rSocketWithTcp("localhost")
deviceManager.connectToMagix(deviceEndpoint)
}
}
fun shutdown() {
logger.info { "Shutting down..." }
magixServer?.stop(1000, 5000)
logger.info { "Magix server stopped" }
magixVirtualCar?.close()
logger.info { "Magix virtual car server stopped" }
virtualCar?.close()
logger.info { "Virtual car server stopped" }
context.close()
}
companion object {
val deviceEntityStorePath = Paths.get(".messages")
val magixEntityStorePath = Paths.get(".server_messages")
}
}
class VirtualCarControllerView : View(title = " Virtual car controller remote") {
private val controller: VirtualCarController by inject()
private var accelerationXProperty: DoubleProperty by singleAssign()
private var accelerationXTF: TextField by singleAssign()
private var accelerationYProperty: DoubleProperty by singleAssign()
private var accelerationYTF: TextField by singleAssign()
override val root: Parent = vbox {
hbox {
label("AccelerationX")
pane {
hgrow = Priority.ALWAYS
}
accelerationXProperty = doubleProperty()
accelerationXTF = textfield(accelerationXProperty)
}
hbox {
label("AccelerationY")
pane {
hgrow = Priority.ALWAYS
}
accelerationYProperty = doubleProperty()
accelerationYTF = textfield(accelerationYProperty)
}
button("Submit") {
useMaxWidth = true
action {
controller.virtualCar?.run {
launch {
acceleration.write(
Vector2D(
accelerationXProperty.get(),
accelerationYProperty.get()
)
)
}
}
}
}
}
}
class VirtualCarControllerApp : App(VirtualCarControllerView::class) {
private val controller: VirtualCarController by inject()
override fun start(stage: Stage) {
super.start(stage)
controller.init()
}
override fun stop() {
controller.shutdown()
super.stop()
}
}
fun main() {
launch<VirtualCarControllerApp>()
}

View File

@ -0,0 +1,32 @@
plugins {
kotlin("jvm")
application
}
repositories {
mavenCentral()
maven("https://repo.kotlin.link")
}
val ktorVersion: String by rootProject.extra
val rsocketVersion: String by rootProject.extra
dependencies {
implementation(projects.magix.magixServer)
implementation(projects.magix.magixRsocket)
implementation(projects.magix.magixZmq)
implementation("io.ktor:ktor-client-cio:$ktorVersion")
implementation("ch.qos.logback:logback-classic:1.2.11")
}
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach {
kotlinOptions {
jvmTarget = "11"
freeCompilerArgs = freeCompilerArgs + listOf("-Xjvm-default=all", "-Xopt-in=kotlin.RequiresOptIn")
}
}
application {
mainClass.set("space.kscience.controls.demo.echo.MainKt")
}

View File

@ -0,0 +1,87 @@
package space.kscience.controls.demo.echo
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.serialization.json.JsonObject
import org.slf4j.LoggerFactory
import space.kscience.magix.api.MagixEndpoint
import space.kscience.magix.api.MagixFlowPlugin
import space.kscience.magix.api.MagixMessage
import space.kscience.magix.api.MagixMessageFilter
import space.kscience.magix.rsocket.rSocketStreamWithWebSockets
import space.kscience.magix.server.startMagixServer
import kotlin.time.ExperimentalTime
import kotlin.time.measureTime
private suspend fun MagixEndpoint.collectEcho(scope: CoroutineScope, n: Int) {
val complete = CompletableDeferred<Boolean>()
val responseIds = HashSet<String>()
scope.launch {
subscribe(
MagixMessageFilter(
origin = listOf("loop")
)
).collect { message ->
if (message.id?.endsWith(".response") == true) {
responseIds.add(message.parentId!!)
}
val parentId = message.parentId
if (parentId != null && parentId.toInt() >= n - 1) {
println("Losses ${(1 - responseIds.size.toDouble() / n) * 100}%")
complete.complete(true)
cancel()
}
}
}
scope.launch {
repeat(n) {
if (it % 20 == 0) delay(1)
broadcast(
MagixMessage(
format = "test",
payload = JsonObject(emptyMap()),
origin = "test",
target = "loop",
id = it.toString()
)
)
}
}
complete.await()
println("completed")
}
@OptIn(ExperimentalTime::class)
suspend fun main(): Unit = coroutineScope {
launch(Dispatchers.Default) {
val server = startMagixServer(MagixFlowPlugin { _, flow ->
val logger = LoggerFactory.getLogger("echo")
//echo each message
flow.onEach { message ->
if (message.parentId == null) {
val m = message.copy(origin = "loop", parentId = message.id, id = message.id + ".response")
logger.info(m.toString())
flow.emit(m)
}
}.launchIn(this)
})
val responseTime = measureTime {
MagixEndpoint.rSocketStreamWithWebSockets("localhost").use {
it.collectEcho(this, 5000)
}
}
println(responseTime)
server.stop(500, 500)
cancel()
}
}

View File

@ -1,5 +1,5 @@
plugins {
id("ru.mipt.npm.gradle.jvm")
id("space.kscience.gradle.jvm")
application
}

View File

@ -7,33 +7,32 @@ import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.isActive
import kotlinx.serialization.json.*
import org.slf4j.LoggerFactory
import ru.mipt.npm.magix.api.MagixEndpoint
import ru.mipt.npm.magix.api.MagixMessage
import ru.mipt.npm.magix.server.startMagixServer
import ru.mipt.npm.magix.zmq.ZmqMagixEndpoint
import space.kscience.magix.api.MagixEndpoint
import space.kscience.magix.api.MagixMessage
import space.kscience.magix.server.RSocketMagixFlowPlugin
import space.kscience.magix.server.startMagixServer
import space.kscince.magix.zmq.ZmqMagixEndpoint
import space.kscince.magix.zmq.ZmqMagixFlowPlugin
import java.awt.Desktop
import java.net.URI
suspend fun MagixEndpoint<JsonObject>.sendJson(
suspend fun MagixEndpoint.sendJson(
origin: String,
format: String = "json",
target: String? = null,
id: String? = null,
parentId: String? = null,
user: JsonElement? = null,
builder: JsonObjectBuilder.() -> Unit
): Unit = broadcast(MagixMessage(format, origin, buildJsonObject(builder), target, id, parentId, user))
builder: JsonObjectBuilder.() -> Unit,
): Unit = broadcast(MagixMessage(format, buildJsonObject(builder), origin, target, id, parentId, user))
internal const val numberOfMessages = 100
suspend fun main(): Unit = coroutineScope {
val logger = LoggerFactory.getLogger("magix-demo")
logger.info("Starting magix server")
val server = startMagixServer(
buffer = 10,
enableRawRSocket = false //Disable rsocket to avoid kotlin 1.5 compatibility issue
)
val server = startMagixServer(RSocketMagixFlowPlugin(), ZmqMagixFlowPlugin(), buffer = 10)
server.apply {
val host = "localhost"//environment.connectors.first().host
@ -44,11 +43,11 @@ suspend fun main(): Unit = coroutineScope {
logger.info("Starting client")
//Create zmq magix endpoint and wait for to finish
ZmqMagixEndpoint("tcp://localhost", JsonObject.serializer()).use { client ->
ZmqMagixEndpoint("localhost", "tcp").use { client ->
logger.info("Starting subscription")
client.subscribe().onEach {
println(it.payload)
if (it.payload["index"]?.jsonPrimitive?.int == numberOfMessages) {
if (it.payload.jsonObject["index"]?.jsonPrimitive?.int == numberOfMessages) {
logger.info("Index $numberOfMessages reached. Terminating")
cancel()
}

View File

@ -0,0 +1,21 @@
plugins {
id("space.kscience.gradle.jvm")
application
}
//TODO to be moved to a separate project
//
//application{
// mainClass.set("ru.mipt.npm.devices.pimotionmaster.PiMotionMasterAppKt")
//}
kotlin{
explicitApi = null
}
val ktorVersion: String by rootProject.extra
val dataforgeVersion: String by extra
dependencies {
implementation(projects.controlsKtorTcp)
}

View File

@ -0,0 +1,109 @@
package center.sciprog.devices.mks
import kotlinx.coroutines.withTimeoutOrNull
import space.kscience.controls.ports.Ports
import space.kscience.controls.ports.SynchronousPort
import space.kscience.controls.ports.respondStringWithDelimiter
import space.kscience.controls.ports.synchronous
import space.kscience.controls.spec.*
import space.kscience.dataforge.context.Context
import space.kscience.dataforge.context.Factory
import space.kscience.dataforge.context.request
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.get
import space.kscience.dataforge.meta.int
import space.kscience.dataforge.meta.transformations.MetaConverter
//TODO this device is not tested
class MksPdr900Device(context: Context, meta: Meta) : DeviceBySpec<MksPdr900Device>(MksPdr900Device, context, meta) {
private val address by meta.int(253)
private val portDelegate = lazy {
val ports = context.request(Ports)
ports.buildPort(meta["port"] ?: error("Port is not defined in device configuration")).synchronous()
}
private val port: SynchronousPort by portDelegate
private val responsePattern: Regex by lazy {
("@${address}ACK(.*);FF").toRegex()
}
private suspend fun talk(requestContent: String): String? = withTimeoutOrNull(5000) {
val answer = port.respondStringWithDelimiter(String.format("@%s%s;FF", address, requestContent), ";FF")
responsePattern.matchEntire(answer)?.groups?.get(1)?.value
?: error("Message $answer does not match $responsePattern")
}
public suspend fun readPowerOn(): Boolean = when (val answer = talk("FP?")) {
"ON" -> true
"OFF" -> false
else -> error("Unknown answer for 'FP?': $answer")
}
public suspend fun writePowerOn(powerOnValue: Boolean) {
error.invalidate()
if (powerOnValue) {
val ans = talk("FP!ON")
if (ans == "ON") {
updateLogical(powerOn, true)
} else {
updateLogical(error, "Failed to set power state")
}
} else {
val ans = talk("FP!OFF")
if (ans == "OFF") {
updateLogical(powerOn, false)
} else {
updateLogical(error, "Failed to set power state")
}
}
}
public suspend fun readChannelData(channel: Int): Double? {
val answer: String? = talk("PR$channel?")
error.invalidate()
return if (answer.isNullOrEmpty()) {
// updateState(PortSensor.CONNECTED_STATE, false)
updateLogical(error, "No connection")
null
} else {
val res = answer.toDouble()
if (res <= 0) {
updateLogical(powerOn, false)
updateLogical(error, "No power")
null
} else {
res
}
}
}
companion object : DeviceSpec<MksPdr900Device>(), Factory<MksPdr900Device> {
const val DEFAULT_CHANNEL: Int = 5
override fun build(context: Context, meta: Meta): MksPdr900Device = MksPdr900Device(context, meta)
val powerOn by booleanProperty(read = MksPdr900Device::readPowerOn, write = MksPdr900Device::writePowerOn)
val channel by logicalProperty(MetaConverter.int)
val value by doubleProperty(read = {
readChannelData(channel.get() ?: DEFAULT_CHANNEL)
})
val error by logicalProperty(MetaConverter.string)
override fun MksPdr900Device.onClose() {
if (portDelegate.isInitialized()) {
port.close()
}
}
}
}

View File

@ -0,0 +1,10 @@
package center.sciprog.devices.mks
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.string
import space.kscience.dataforge.meta.transformations.MetaConverter
object NullableStringMetaConverter : MetaConverter<String?> {
override fun metaToObject(meta: Meta): String? = meta.string
override fun objectToMeta(obj: String?): Meta = Meta {}
}

View File

@ -1,11 +1,16 @@
plugins {
id("ru.mipt.npm.gradle.jvm")
`maven-publish`
id("space.kscience.gradle.jvm")
application
id("org.openjfx.javafxplugin")
}
//TODO to be moved to a separate project
javafx {
version = "17"
modules = listOf("javafx.controls")
}
application{
mainClass.set("ru.mipt.npm.devices.pimotionmaster.PiMotionMasterAppKt")
}
@ -14,14 +19,11 @@ kotlin{
explicitApi = null
}
kscience{
useFx(ru.mipt.npm.gradle.FXModule.CONTROLS, configuration = ru.mipt.npm.gradle.DependencyConfiguration.IMPLEMENTATION)
}
val ktorVersion: String by rootProject.extra
val dataforgeVersion: String by extra
dependencies {
implementation(project(":controls-tcp"))
implementation(project(":controls-ktor-tcp"))
implementation(project(":controls-magix-client"))
implementation("no.tornado:tornadofx:1.7.20")
}

View File

@ -11,9 +11,12 @@ import javafx.scene.layout.VBox
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import ru.mipt.npm.controls.controllers.DeviceManager
import ru.mipt.npm.controls.controllers.installing
import space.kscience.dataforge.context.Global
import ru.mipt.npm.devices.pimotionmaster.PiMotionMasterDevice.Axis.Companion.maxPosition
import ru.mipt.npm.devices.pimotionmaster.PiMotionMasterDevice.Axis.Companion.minPosition
import ru.mipt.npm.devices.pimotionmaster.PiMotionMasterDevice.Axis.Companion.position
import space.kscience.controls.manager.DeviceManager
import space.kscience.controls.manager.installing
import space.kscience.dataforge.context.Context
import space.kscience.dataforge.context.fetch
import tornadofx.*
@ -21,7 +24,7 @@ class PiMotionMasterApp : App(PiMotionMasterView::class)
class PiMotionMasterController : Controller() {
//initialize context
val context = Global.buildContext("piMotionMaster"){
val context = Context("piMotionMaster"){
plugin(DeviceManager)
}
@ -40,28 +43,30 @@ fun VBox.piMotionMasterAxis(
alignment = Pos.CENTER
label(axisName)
coroutineScope.launch {
val min = axis.minPosition.readTyped(true)
val max = axis.maxPosition.readTyped(true)
val positionProperty = axis.position.fxProperty(axis)
val startPosition = axis.position.readTyped(true)
runLater {
vbox {
hgrow = Priority.ALWAYS
slider(min..max, startPosition) {
minWidth = 300.0
isShowTickLabels = true
isShowTickMarks = true
minorTickCount = 10
majorTickUnit = 1.0
valueProperty().onChange {
coroutineScope.launch {
axis.move(value)
with(axis) {
val min: Double = minPosition.read()
val max: Double = maxPosition.read()
val positionProperty = fxProperty(position)
val startPosition = position.read()
runLater {
vbox {
hgrow = Priority.ALWAYS
slider(min..max, startPosition) {
minWidth = 300.0
isShowTickLabels = true
isShowTickMarks = true
minorTickCount = 10
majorTickUnit = 1.0
valueProperty().onChange {
coroutineScope.launch {
axis.move(value)
}
}
}
}
slider(min..max) {
isDisable = true
valueProperty().bind(positionProperty)
slider(min..max) {
isDisable = true
valueProperty().bind(positionProperty)
}
}
}
}
@ -82,7 +87,7 @@ class PiMotionMasterView : View() {
private val controller: PiMotionMasterController by inject()
val device = controller.motionMaster
private val connectedProperty: ReadOnlyProperty<Boolean> = device.connected.fxProperty(device)
private val connectedProperty: ReadOnlyProperty<Boolean> = device.fxProperty(PiMotionMasterDevice.connected)
private val debugServerJobProperty = SimpleObjectProperty<Job>()
private val debugServerStarted = debugServerJobProperty.booleanBinding { it != null }
//private val axisList = FXCollections.observableArrayList<Map.Entry<String, PiMotionMasterDevice.Axis>>()

View File

@ -0,0 +1,357 @@
@file:Suppress("unused", "MemberVisibilityCanBePrivate")
package ru.mipt.npm.devices.pimotionmaster
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.flow.transformWhile
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withTimeout
import space.kscience.controls.api.DeviceHub
import space.kscience.controls.api.PropertyDescriptor
import space.kscience.controls.ports.*
import space.kscience.controls.spec.*
import space.kscience.dataforge.context.*
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.asValue
import space.kscience.dataforge.meta.double
import space.kscience.dataforge.meta.get
import space.kscience.dataforge.meta.transformations.MetaConverter
import space.kscience.dataforge.names.NameToken
import kotlin.collections.component1
import kotlin.collections.component2
import kotlin.time.Duration
import kotlin.time.Duration.Companion.milliseconds
class PiMotionMasterDevice(
context: Context,
private val portFactory: PortFactory = KtorTcpPort,
) : DeviceBySpec<PiMotionMasterDevice>(PiMotionMasterDevice, context), DeviceHub {
private var port: Port? = null
//TODO make proxy work
//PortProxy { portFactory(address ?: error("The device is not connected"), context) }
fun disconnect() {
runBlocking {
disconnect.invoke()
}
}
var timeoutValue: Duration = 200.milliseconds
/**
* Name-friendly accessor for axis
*/
var axes: Map<String, Axis> = emptyMap()
private set
override val devices: Map<NameToken, Axis> = axes.mapKeys { (key, _) -> NameToken(key) }
private suspend fun failIfError(message: (Int) -> String = { "Failed with error code $it" }) {
val errorCode = getErrorCode()
if (errorCode != 0) error(message(errorCode))
}
fun connect(host: String, port: Int) {
runBlocking {
connect(Meta {
"host" put host
"port" put port
})
}
}
private val mutex = Mutex()
private suspend fun dispatchError(errorCode: Int) {
logger.error { "Error code: $errorCode" }
//TODO add error handling
}
private suspend fun sendCommandInternal(command: String, vararg arguments: String) {
val joinedArguments = if (arguments.isEmpty()) {
""
} else {
arguments.joinToString(prefix = " ", separator = " ", postfix = "")
}
val stringToSend = "$command$joinedArguments\n"
port?.send(stringToSend) ?: error("Not connected to device")
}
suspend fun getErrorCode(): Int = mutex.withLock {
withTimeout(timeoutValue) {
sendCommandInternal("ERR?")
val errorString = port?.receiving()?.withStringDelimiter("\n")?.first() ?: error("Not connected to device")
errorString.trim().toInt()
}
}
/**
* Send a synchronous request and receive a list of lines as a response
*/
@OptIn(ExperimentalCoroutinesApi::class)
private suspend fun request(command: String, vararg arguments: String): List<String> = mutex.withLock {
try {
withTimeout(timeoutValue) {
sendCommandInternal(command, *arguments)
val phrases = port?.receiving()?.withStringDelimiter("\n") ?: error("Not connected to device")
phrases.transformWhile { line ->
emit(line)
line.endsWith(" \n")
}.toList()
}
} catch (ex: Throwable) {
logger.warn { "Error during PIMotionMaster request. Requesting error code." }
val errorCode = getErrorCode()
dispatchError(errorCode)
logger.warn { "Error code $errorCode" }
error("Error code $errorCode")
}
}
private suspend fun requestAndParse(command: String, vararg arguments: String): Map<String, String> = buildMap {
request(command, *arguments).forEach { line ->
val (key, value) = line.split("=")
put(key, value.trim())
}
}
/**
* Send a synchronous command
*/
private suspend fun send(command: String, vararg arguments: String) {
mutex.withLock {
withTimeout(timeoutValue) {
sendCommandInternal(command, *arguments)
}
}
}
companion object : DeviceSpec<PiMotionMasterDevice>(), Factory<PiMotionMasterDevice> {
override fun build(context: Context, meta: Meta): PiMotionMasterDevice = PiMotionMasterDevice(context)
val connected by booleanProperty(descriptorBuilder = {
info = "True if the connection address is defined and the device is initialized"
}) {
port != null
}
val initialize by unitAction {
send("INI")
}
val identity by stringProperty {
request("*IDN?").first()
}
val firmwareVersion by stringProperty {
request("VER?").first()
}
val stop by unitAction({
info = "Stop all axis"
}) {
send("STP")
}
val connect by metaAction(descriptorBuilder = {
info = "Connect to specific port and initialize axis"
}) { portSpec ->
//Clear current actions if present
if (port != null) {
disconnect()
}
//Update port
//address = portSpec.node
port = portFactory(portSpec ?: Meta.EMPTY, context)
updateLogical(connected, true)
// connector.open()
//Initialize axes
if (portSpec != null) {
val idn = identity.read()
failIfError { "Can't connect to $portSpec. Error code: $it" }
logger.info { "Connected to $idn on $portSpec" }
val ids = request("SAI?").map { it.trim() }
if (ids != axes.keys.toList()) {
//re-define axes if needed
axes = ids.associateWith { Axis(this, it) }
}
Meta(ids.map { it.asValue() }.asValue())
initialize()
failIfError()
}
null
}
val disconnect by metaAction({
info = "Disconnect the program from the device if it is connected"
}) {
port?.let{
stop()
it.close()
}
port = null
updateLogical(connected, false)
null
}
val timeout by mutableProperty(MetaConverter.duration, PiMotionMasterDevice::timeoutValue) {
info = "Timeout"
}
}
class Axis(
val mm: PiMotionMasterDevice,
val axisId: String
) : DeviceBySpec<Axis>(Axis, mm.context) {
/**
* TODO Move to head device and abstract
*/
private suspend fun readAxisBoolean(command: String): Boolean =
(mm.requestAndParse(command, axisId)[axisId]?.toIntOrNull()
?: error("Malformed $command response. Should include integer value for $axisId")) != 0
/**
* TODO Move to head device and abstract
*/
private suspend fun writeAxisBoolean(command: String, value: Boolean): Boolean {
val boolean = if (value) {
"1"
} else {
"0"
}
mm.send(command, axisId, boolean)
mm.failIfError()
return value
}
suspend fun move(target: Double) {
move(target.asMeta())
}
companion object : DeviceSpec<Axis>() {
private fun axisBooleanProperty(
command: String,
descriptorBuilder: PropertyDescriptor.() -> Unit = {}
) = booleanProperty(
read = {
readAxisBoolean("$command?")
},
write = {
writeAxisBoolean(command, it)
},
descriptorBuilder = descriptorBuilder
)
private fun axisNumberProperty(
command: String,
descriptorBuilder: PropertyDescriptor.() -> Unit = {}
) = doubleProperty(
read = {
mm.requestAndParse("$command?", axisId)[axisId]?.toDoubleOrNull()
?: error("Malformed $command response. Should include float value for $axisId")
},
write = { newValue ->
mm.send(command, axisId, newValue.toString())
mm.failIfError()
},
descriptorBuilder = descriptorBuilder
)
val enabled by axisBooleanProperty("EAX") {
info = "Motor enable state."
}
val halt by unitAction {
mm.send("HLT", axisId)
}
val targetPosition by axisNumberProperty("MOV") {
info = """
Sets a new absolute target position for the specified axis.
Servo mode must be switched on for the commanded axis prior to using this command (closed-loop operation).
""".trimIndent()
}
val onTarget by booleanProperty({
info = "Queries the on-target state of the specified axis."
}) {
readAxisBoolean("ONT?")
}
val reference by booleanProperty({
info = "Get Referencing Result"
}) {
readAxisBoolean("FRF?")
}
val moveToReference by unitAction {
mm.send("FRF", axisId)
}
val minPosition by doubleProperty({
info = "Minimal position value for the axis"
}) {
mm.requestAndParse("TMN?", axisId)[axisId]?.toDoubleOrNull()
?: error("Malformed `TMN?` response. Should include float value for $axisId")
}
val maxPosition by doubleProperty({
info = "Maximal position value for the axis"
}) {
mm.requestAndParse("TMX?", axisId)[axisId]?.toDoubleOrNull()
?: error("Malformed `TMX?` response. Should include float value for $axisId")
}
val position by doubleProperty({
info = "The current axis position."
}) {
mm.requestAndParse("POS?", axisId)[axisId]?.toDoubleOrNull()
?: error("Malformed `POS?` response. Should include float value for $axisId")
}
val openLoopTarget by axisNumberProperty("OMA") {
info = "Position for open-loop operation."
}
val closedLoop by axisBooleanProperty("SVO") {
info = "Servo closed loop mode"
}
val velocity by axisNumberProperty("VEL") {
info = "Velocity value for closed-loop operation"
}
val move by metaAction {
val target = it.double ?: it?.get("target").double ?: error("Unacceptable target value $it")
closedLoop.write(true)
//optionally set velocity
it?.get("velocity").double?.let { v ->
velocity.write(v)
}
targetPosition.write(target)
//read `onTarget` and `position` properties in a cycle until movement is complete
while (!onTarget.read()) {
position.read()
delay(200)
}
null
}
}
}
}

View File

@ -5,9 +5,9 @@ import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import ru.mipt.npm.controls.api.Socket
import ru.mipt.npm.controls.ports.AbstractPort
import ru.mipt.npm.controls.ports.withDelimiter
import space.kscience.controls.api.Socket
import space.kscience.controls.ports.AbstractPort
import space.kscience.controls.ports.withDelimiter
import space.kscience.dataforge.context.*
import kotlin.math.abs
import kotlin.time.Duration

View File

@ -0,0 +1,66 @@
package ru.mipt.npm.devices.pimotionmaster
import javafx.beans.property.ObjectPropertyBase
import javafx.beans.property.Property
import javafx.beans.property.ReadOnlyProperty
import space.kscience.controls.api.Device
import space.kscience.controls.spec.*
import space.kscience.dataforge.context.info
import space.kscience.dataforge.context.logger
import tornadofx.*
/**
* Bind a FX property to a device property with a given [spec]
*/
fun <D : Device, T : Any> Device.fxProperty(
spec: DevicePropertySpec<D, T>
): ReadOnlyProperty<T> = object : ObjectPropertyBase<T>() {
override fun getBean(): Any = this
override fun getName(): String = spec.name
init {
//Read incoming changes
onPropertyChange(spec) {
if (it != null) {
runLater {
try {
set(it)
} catch (ex: Throwable) {
logger.info { "Failed to set property $name to $it" }
}
}
} else {
invalidated()
}
}
}
}
fun <D : Device, T : Any> D.fxProperty(spec: WritableDevicePropertySpec<D, T>): Property<T> =
object : ObjectPropertyBase<T>() {
override fun getBean(): Any = this
override fun getName(): String = spec.name
init {
//Read incoming changes
onPropertyChange(spec) {
if (it != null) {
runLater {
try {
set(it)
} catch (ex: Throwable) {
logger.info { "Failed to set property $name to $it" }
}
}
} else {
invalidated()
}
}
onChange { newValue ->
if (newValue != null) {
write(spec, newValue)
}
}
}
}

View File

@ -8,10 +8,8 @@ import io.ktor.util.InternalAPI
import io.ktor.util.moveToByteArray
import io.ktor.utils.io.writeAvailable
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.collect
import space.kscience.dataforge.context.Context
import space.kscience.dataforge.context.Global
import java.net.InetSocketAddress
val exceptionHandler = CoroutineExceptionHandler { _, throwable ->
throwable.printStackTrace()
@ -20,7 +18,7 @@ val exceptionHandler = CoroutineExceptionHandler { _, throwable ->
@OptIn(InternalAPI::class)
fun Context.launchPiDebugServer(port: Int, axes: List<String>): Job = launch(exceptionHandler) {
val virtualDevice = PiMotionMasterVirtualDevice(this@launchPiDebugServer, axes)
val server = aSocket(ActorSelectorManager(Dispatchers.IO)).tcp().bind(InetSocketAddress("localhost", port))
val server = aSocket(ActorSelectorManager(Dispatchers.IO)).tcp().bind("localhost", port)
println("Started virtual port server at ${server.localAddress}")
while (isActive) {

View File

@ -1,60 +0,0 @@
package ru.mipt.npm.controls.demo
import kotlinx.coroutines.launch
import ru.mipt.npm.controls.properties.*
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.transformations.MetaConverter
import java.time.Instant
import kotlin.time.Duration
import kotlin.time.ExperimentalTime
class DemoDevice : DeviceBySpec<DemoDevice>(DemoDevice) {
private var timeScaleState = 5000.0
private var sinScaleState = 1.0
private var cosScaleState = 1.0
companion object : DeviceSpec<DemoDevice>(::DemoDevice) {
// register virtual properties based on actual object state
val timeScale by property(MetaConverter.double, DemoDevice::timeScaleState)
val sinScale by property(MetaConverter.double, DemoDevice::sinScaleState)
val cosScale by property(MetaConverter.double, DemoDevice::cosScaleState)
val sin by doubleProperty {
val time = Instant.now()
kotlin.math.sin(time.toEpochMilli().toDouble() / timeScaleState) * sinScaleState
}
val cos by doubleProperty {
val time = Instant.now()
kotlin.math.cos(time.toEpochMilli().toDouble() / timeScaleState) * sinScaleState
}
val coordinates by metaProperty {
Meta {
val time = Instant.now()
"time" put time.toEpochMilli()
"x" put read(sin)
"y" put read(cos)
}
}
val resetScale by action(MetaConverter.meta, MetaConverter.meta) {
timeScale.write(5000.0)
sinScale.write(1.0)
cosScale.write(1.0)
null
}
@OptIn(ExperimentalTime::class)
override fun DemoDevice.onStartup() {
launch {
sinScale.read()
cosScale.read()
}
doRecurring(Duration.milliseconds(50)){
coordinates.read()
}
}
}
}

View File

@ -1,10 +0,0 @@
package ru.mipt.npm.controls.demo
import com.github.ricky12awesome.jss.encodeToSchema
import com.github.ricky12awesome.jss.globalJson
import ru.mipt.npm.controls.api.DeviceMessage
fun main() {
val schema = globalJson.encodeToSchema(DeviceMessage.serializer(), generateDefinitions = false)
println(schema)
}

10
gradle.properties Normal file
View File

@ -0,0 +1,10 @@
kotlin.code.style=official
kotlin.mpp.stability.nowarn=true
kotlin.native.ignoreDisabledTargets=true
org.gradle.parallel=true
publishing.github=false
publishing.sonatype=false
toolsVersion=0.14.3-kotlin-1.8.10

View File

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

View File

@ -1,9 +1,12 @@
plugins {
id("ru.mipt.npm.gradle.mpp")
id("space.kscience.gradle.mpp")
`maven-publish`
}
kscience {
jvm()
js()
native()
useCoroutines()
useSerialization{
json()

Some files were not shown because too many files have changed in this diff Show More