Dev #8
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
[submodule "magix/rfc"]
|
||||
path = magix/rfc
|
||||
url = https://github.com/waltz-controls/rfc
|
1
.space/CODEOWNERS
Normal file
1
.space/CODEOWNERS
Normal file
@ -0,0 +1 @@
|
||||
./space/* "Project Admin"
|
@ -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 {
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
@ -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?
|
||||
}
|
@ -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 {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -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)
|
||||
}
|
@ -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))
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
@ -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
|
||||
)
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
@ -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) }
|
||||
}
|
@ -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")
|
||||
}
|
||||
@ -87,7 +101,7 @@ public suspend fun Device.getOrReadProperty(propertyName: String): Meta =
|
||||
*
|
||||
* 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))
|
||||
}
|
@ -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.*
|
@ -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)
|
@ -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 {
|
||||
/**
|
@ -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
|
@ -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
|
||||
}
|
||||
|
@ -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 {
|
@ -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()
|
@ -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
|
@ -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()
|
||||
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
@ -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)
|
@ -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)
|
||||
}
|
@ -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
|
||||
}
|
@ -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)
|
@ -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
|
||||
}
|
||||
}
|
@ -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()
|
@ -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
|
@ -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)
|
@ -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)
|
@ -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) }
|
@ -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
|
@ -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)
|
@ -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()
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -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) }
|
@ -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
|
10
controls-ktor-tcp/build.gradle.kts
Normal file
10
controls-ktor-tcp/build.gradle.kts
Normal 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")
|
||||
}
|
@ -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)
|
@ -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()
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -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"))
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
package ru.mipt.npm.controls.client
|
||||
package space.kscience.controls.client
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
@ -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)
|
@ -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")
|
||||
|
@ -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.*
|
||||
|
||||
|
@ -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")
|
||||
}
|
||||
|
@ -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
|
@ -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
|
@ -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() }
|
@ -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
|
||||
|
||||
/**
|
@ -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
|
@ -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
|
@ -1,5 +1,5 @@
|
||||
plugins {
|
||||
id("ru.mipt.npm.gradle.jvm")
|
||||
id("space.kscience.gradle.jvm")
|
||||
`maven-publish`
|
||||
}
|
||||
|
||||
|
@ -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)
|
@ -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()
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -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")
|
||||
}
|
@ -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)
|
||||
}
|
@ -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 {
|
12
controls-storage/README.md
Normal file
12
controls-storage/README.md
Normal 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.
|
23
controls-storage/build.gradle.kts
Normal file
23
controls-storage/build.gradle.kts
Normal 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
|
||||
}
|
19
controls-storage/controls-xodus/build.gradle.kts
Normal file
19
controls-storage/controls-xodus/build.gradle.kts
Normal 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
|
||||
}
|
@ -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
|
||||
}
|
@ -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" }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
@ -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
|
||||
//}
|
||||
|
@ -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()
|
||||
// }
|
||||
//}
|
@ -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())
|
||||
}
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -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")
|
||||
}
|
@ -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)
|
||||
}
|
@ -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
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -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() }
|
||||
|
@ -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
52
demo/car/build.gradle.kts
Normal 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")
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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>()
|
||||
}
|
32
demo/echo/build.gradle.kts
Normal file
32
demo/echo/build.gradle.kts
Normal 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")
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
plugins {
|
||||
id("ru.mipt.npm.gradle.jvm")
|
||||
id("space.kscience.gradle.jvm")
|
||||
application
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
21
demo/mks-pdr900/build.gradle.kts
Normal file
21
demo/mks-pdr900/build.gradle.kts
Normal 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)
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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 {}
|
||||
}
|
@ -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")
|
||||
}
|
@ -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>>()
|
@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -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
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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) {
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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
10
gradle.properties
Normal 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
|
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@ -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
|
||||
|
@ -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
Loading…
Reference in New Issue
Block a user