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 {
|
plugins {
|
||||||
id("ru.mipt.npm.gradle.project")
|
id("space.kscience.gradle.project")
|
||||||
}
|
}
|
||||||
|
|
||||||
val dataforgeVersion: String by extra("0.5.1")
|
val dataforgeVersion: String by extra("0.6.1-dev-4")
|
||||||
val ktorVersion: String by extra(ru.mipt.npm.gradle.KScienceVersions.ktorVersion)
|
val ktorVersion: String by extra(space.kscience.gradle.KScienceVersions.ktorVersion)
|
||||||
val rsocketVersion by extra("0.13.1")
|
val rsocketVersion by extra("0.15.4")
|
||||||
|
val xodusVersion by extra("2.0.1")
|
||||||
|
|
||||||
allprojects {
|
allprojects {
|
||||||
group = "ru.mipt.npm"
|
group = "space.kscience"
|
||||||
version = "0.1.1"
|
version = "0.1.1-SNAPSHOT"
|
||||||
|
repositories{
|
||||||
|
maven("https://maven.pkg.jetbrains.space/spc/p/sci/dev")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ksciencePublish {
|
ksciencePublish {
|
||||||
github("controls.kt")
|
pom("https://github.com/SciProgCentre/controls.kt") {
|
||||||
space()
|
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 {
|
apiValidation {
|
||||||
|
@ -1,24 +1,20 @@
|
|||||||
plugins {
|
plugins {
|
||||||
id("ru.mipt.npm.gradle.mpp")
|
id("space.kscience.gradle.mpp")
|
||||||
`maven-publish`
|
`maven-publish`
|
||||||
}
|
}
|
||||||
|
|
||||||
val dataforgeVersion: String by rootProject.extra
|
val dataforgeVersion: String by rootProject.extra
|
||||||
|
|
||||||
kscience {
|
kscience {
|
||||||
useCoroutines("1.4.1")
|
jvm()
|
||||||
|
js()
|
||||||
|
native()
|
||||||
|
useCoroutines()
|
||||||
useSerialization{
|
useSerialization{
|
||||||
json()
|
json()
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
kotlin {
|
|
||||||
sourceSets {
|
|
||||||
commonMain{
|
|
||||||
dependencies {
|
dependencies {
|
||||||
api("space.kscience:dataforge-io:$dataforgeVersion")
|
api("space.kscience:dataforge-io:$dataforgeVersion")
|
||||||
api(npm.kotlinx.datetime)
|
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 io.ktor.utils.io.core.Closeable
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
@ -8,7 +8,7 @@ import kotlinx.coroutines.flow.Flow
|
|||||||
import kotlinx.coroutines.flow.filterIsInstance
|
import kotlinx.coroutines.flow.filterIsInstance
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.onEach
|
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.context.ContextAware
|
||||||
import space.kscience.dataforge.meta.Meta
|
import space.kscience.dataforge.meta.Meta
|
||||||
import space.kscience.dataforge.misc.Type
|
import space.kscience.dataforge.misc.Type
|
||||||
@ -21,6 +21,12 @@ import space.kscience.dataforge.names.Name
|
|||||||
*/
|
*/
|
||||||
@Type(DEVICE_TARGET)
|
@Type(DEVICE_TARGET)
|
||||||
public interface Device : Closeable, ContextAware, CoroutineScope {
|
public interface Device : Closeable, ContextAware, CoroutineScope {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initial configuration meta for the device
|
||||||
|
*/
|
||||||
|
public val meta: Meta get() = Meta.EMPTY
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* List of supported property descriptors
|
* List of supported property descriptors
|
||||||
*/
|
*/
|
||||||
@ -51,13 +57,13 @@ public interface Device : Closeable, ContextAware, CoroutineScope {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Set property [value] for a property with name [propertyName].
|
* 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)
|
public suspend fun writeProperty(propertyName: String, value: Meta)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A subscription-based [Flow] of [DeviceMessage] provided by device. The flow is guaranteed to be readable
|
* 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>
|
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?
|
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() {
|
override fun close() {
|
||||||
cancel("The device is closed")
|
cancel("The device is closed")
|
||||||
}
|
}
|
||||||
@ -87,7 +101,7 @@ public suspend fun Device.getOrReadProperty(propertyName: String): Meta =
|
|||||||
*
|
*
|
||||||
* TODO currently this
|
* TODO currently this
|
||||||
*/
|
*/
|
||||||
public fun Device.getProperties(): Meta = Meta {
|
public fun Device.getAllProperties(): Meta = Meta {
|
||||||
for (descriptor in propertyDescriptors) {
|
for (descriptor in propertyDescriptors) {
|
||||||
setMeta(Name.parse(descriptor.name), getProperty(descriptor.name))
|
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.meta.Meta
|
||||||
import space.kscience.dataforge.names.*
|
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.Clock
|
||||||
import kotlinx.datetime.Instant
|
import kotlinx.datetime.Instant
|
||||||
@ -7,7 +7,7 @@ import kotlinx.serialization.Serializable
|
|||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import kotlinx.serialization.json.decodeFromJsonElement
|
import kotlinx.serialization.json.decodeFromJsonElement
|
||||||
import kotlinx.serialization.json.encodeToJsonElement
|
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.Meta
|
||||||
import space.kscience.dataforge.meta.toJson
|
import space.kscience.dataforge.meta.toJson
|
||||||
import space.kscience.dataforge.meta.toMeta
|
import space.kscience.dataforge.meta.toMeta
|
||||||
@ -113,6 +113,8 @@ public data class GetDescriptionMessage(
|
|||||||
@SerialName("description")
|
@SerialName("description")
|
||||||
public data class DescriptionMessage(
|
public data class DescriptionMessage(
|
||||||
val description: Meta,
|
val description: Meta,
|
||||||
|
val properties: Collection<PropertyDescriptor>,
|
||||||
|
val actions: Collection<ActionDescriptor>,
|
||||||
override val sourceDevice: Name,
|
override val sourceDevice: Name,
|
||||||
override val targetDevice: Name? = null,
|
override val targetDevice: Name? = null,
|
||||||
override val comment: String? = 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.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 io.ktor.utils.io.core.Closeable
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.collect
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A generic bi-directional sender/receiver object
|
* A generic bidirectional sender/receiver object
|
||||||
*/
|
*/
|
||||||
public interface Socket<T> : Closeable {
|
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 kotlinx.serialization.Serializable
|
||||||
import space.kscience.dataforge.meta.descriptors.MetaDescriptor
|
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 kotlinx.coroutines.launch
|
||||||
import ru.mipt.npm.controls.api.DeviceHub
|
import space.kscience.controls.api.Device
|
||||||
|
import space.kscience.controls.api.DeviceHub
|
||||||
import space.kscience.dataforge.context.*
|
import space.kscience.dataforge.context.*
|
||||||
import space.kscience.dataforge.meta.Meta
|
import space.kscience.dataforge.meta.Meta
|
||||||
import space.kscience.dataforge.meta.MutableMeta
|
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 tag: PluginTag = PluginTag("devices", group = PluginTag.DATAFORGE_GROUP)
|
||||||
override val type: KClass<out DeviceManager> = DeviceManager::class
|
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 {
|
public fun <D : Device> DeviceManager.install(name: String, factory: Factory<D>, meta: Meta = Meta.EMPTY): D {
|
||||||
val device = factory(meta, context)
|
val device = factory(meta, context)
|
||||||
registerDevice(NameToken(name), device)
|
registerDevice(NameToken(name), device)
|
||||||
|
device.launch {
|
||||||
|
device.open()
|
||||||
|
}
|
||||||
return device
|
return device
|
||||||
}
|
}
|
||||||
|
|
@ -1,15 +1,11 @@
|
|||||||
package ru.mipt.npm.controls.controllers
|
package space.kscience.controls.manager
|
||||||
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
import kotlinx.serialization.json.Json
|
import space.kscience.controls.api.*
|
||||||
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.dataforge.names.Name
|
import space.kscience.dataforge.names.Name
|
||||||
import space.kscience.dataforge.names.plus
|
import space.kscience.dataforge.names.plus
|
||||||
|
|
||||||
@ -48,21 +44,10 @@ public suspend fun Device.respondMessage(deviceTarget: Name, request: DeviceMess
|
|||||||
}
|
}
|
||||||
|
|
||||||
is GetDescriptionMessage -> {
|
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(
|
DescriptionMessage(
|
||||||
description = descriptionMeta,
|
description = meta,
|
||||||
|
properties = propertyDescriptors,
|
||||||
|
actions = actionDescriptors,
|
||||||
sourceDevice = deviceTarget,
|
sourceDevice = deviceTarget,
|
||||||
targetDevice = request.sourceDevice
|
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
|
* Collect all messages from given [DeviceHub], applying proper relative names
|
||||||
*/
|
*/
|
||||||
public fun DeviceHub.hubMessageFlow(scope: CoroutineScope): Flow<DeviceMessage> {
|
public fun DeviceHub.hubMessageFlow(scope: CoroutineScope): Flow<DeviceMessage> {
|
||||||
|
|
||||||
|
//TODO could we avoid using downstream scope?
|
||||||
val outbox = MutableSharedFlow<DeviceMessage>()
|
val outbox = MutableSharedFlow<DeviceMessage>()
|
||||||
if (this is Device) {
|
if (this is Device) {
|
||||||
messageFlow.onEach {
|
messageFlow.onEach {
|
@ -1,16 +1,24 @@
|
|||||||
package ru.mipt.npm.controls.ports
|
package space.kscience.controls.ports
|
||||||
|
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.coroutines.channels.Channel
|
import kotlinx.coroutines.channels.Channel
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.receiveAsFlow
|
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.context.*
|
||||||
|
import space.kscience.dataforge.misc.Type
|
||||||
import kotlin.coroutines.CoroutineContext
|
import kotlin.coroutines.CoroutineContext
|
||||||
|
|
||||||
public interface Port : ContextAware, Socket<ByteArray>
|
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(
|
public abstract class AbstractPort(
|
||||||
override val context: Context,
|
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.
|
* 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.
|
* For example [delimitedIncoming] generates phrases with fixed delimiter.
|
||||||
*/
|
*/
|
||||||
override fun receiving(): Flow<ByteArray> {
|
override fun receiving(): Flow<ByteArray> = incoming.receiveAsFlow()
|
||||||
return incoming.receiveAsFlow()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun close() {
|
override fun close() {
|
||||||
outgoing.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.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.collect
|
|
||||||
import kotlinx.coroutines.flow.flow
|
import kotlinx.coroutines.flow.flow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.sync.Mutex
|
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.BytePacketBuilder
|
||||||
import io.ktor.utils.io.core.readBytes
|
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.
|
* 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" }
|
require(delimiter.isNotEmpty()) { "Delimiter must not be empty" }
|
||||||
|
|
||||||
val output = BytePacketBuilder(expectedMessageSize)
|
val output = BytePacketBuilder()
|
||||||
var matcherPosition = 0
|
var matcherPosition = 0
|
||||||
|
|
||||||
return transform { chunk ->
|
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
|
* Transform byte fragments into utf-8 phrases using utf-8 delimiter
|
||||||
*/
|
*/
|
||||||
public fun Flow<ByteArray>.withDelimiter(delimiter: String, expectedMessageSize: Int = 32): Flow<String> {
|
public fun Flow<ByteArray>.withStringDelimiter(delimiter: String): Flow<String> {
|
||||||
return withDelimiter(delimiter.encodeToByteArray(), expectedMessageSize).map { it.decodeToString() }
|
return withDelimiter(delimiter.encodeToByteArray()).map { it.decodeToString() }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A flow of delimited phrases
|
* A flow of delimited phrases
|
||||||
*/
|
*/
|
||||||
public suspend fun Port.delimitedIncoming(delimiter: ByteArray, expectedMessageSize: Int = 32): Flow<ByteArray> =
|
public fun Port.delimitedIncoming(delimiter: ByteArray): Flow<ByteArray> = receiving().withDelimiter(delimiter)
|
||||||
receiving().withDelimiter(delimiter, expectedMessageSize)
|
|
@ -1,4 +1,4 @@
|
|||||||
package ru.mipt.npm.controls.properties
|
package space.kscience.controls.spec
|
||||||
|
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.SupervisorJob
|
import kotlinx.coroutines.SupervisorJob
|
||||||
@ -7,30 +7,28 @@ import kotlinx.coroutines.flow.SharedFlow
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.sync.Mutex
|
import kotlinx.coroutines.sync.Mutex
|
||||||
import kotlinx.coroutines.sync.withLock
|
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.Context
|
||||||
import space.kscience.dataforge.context.Global
|
import space.kscience.dataforge.context.Global
|
||||||
import space.kscience.dataforge.meta.Meta
|
import space.kscience.dataforge.meta.Meta
|
||||||
import kotlin.coroutines.CoroutineContext
|
import kotlin.coroutines.CoroutineContext
|
||||||
|
|
||||||
/**
|
|
||||||
* A device generated from specification
|
|
||||||
* @param D recursive self-type for properties and actions
|
|
||||||
*/
|
|
||||||
@OptIn(InternalDeviceAPI::class)
|
@OptIn(InternalDeviceAPI::class)
|
||||||
public open class DeviceBySpec<D : DeviceBySpec<D>>(
|
public abstract class DeviceBase<D : DeviceBase<D>>(
|
||||||
public val spec: DeviceSpec<D>,
|
override val context: Context = Global,
|
||||||
context: Context = Global,
|
override val meta: Meta = Meta.EMPTY,
|
||||||
meta: Meta = Meta.EMPTY
|
|
||||||
) : Device {
|
) : 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>
|
override val propertyDescriptors: Collection<PropertyDescriptor>
|
||||||
get() = properties.values.map { it.descriptor }
|
get() = properties.values.map { it.descriptor }
|
||||||
@ -42,6 +40,9 @@ public open class DeviceBySpec<D : DeviceBySpec<D>>(
|
|||||||
context.coroutineContext + SupervisorJob(context.coroutineContext[Job])
|
context.coroutineContext + SupervisorJob(context.coroutineContext[Job])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Logical state store
|
||||||
|
*/
|
||||||
private val logicalState: HashMap<String, Meta?> = HashMap()
|
private val logicalState: HashMap<String, Meta?> = HashMap()
|
||||||
|
|
||||||
private val sharedMessageFlow: MutableSharedFlow<DeviceMessage> = MutableSharedFlow()
|
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.
|
* 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
|
* 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? =
|
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 {
|
public suspend fun <T> DevicePropertySpec<D, T>.readOrNull(): T? {
|
||||||
val res = read(self)
|
val res = read(self) ?: return null
|
||||||
updateLogical(name, converter.objectToMeta(res))
|
updateLogical(name, converter.objectToMeta(res))
|
||||||
return 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)
|
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()
|
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 kotlinx.coroutines.withContext
|
||||||
import ru.mipt.npm.controls.api.ActionDescriptor
|
import space.kscience.controls.api.ActionDescriptor
|
||||||
import ru.mipt.npm.controls.api.PropertyDescriptor
|
import space.kscience.controls.api.Device
|
||||||
import space.kscience.dataforge.context.Context
|
import space.kscience.controls.api.PropertyDescriptor
|
||||||
import space.kscience.dataforge.context.Factory
|
|
||||||
import space.kscience.dataforge.meta.Meta
|
import space.kscience.dataforge.meta.Meta
|
||||||
import space.kscience.dataforge.meta.transformations.MetaConverter
|
import space.kscience.dataforge.meta.transformations.MetaConverter
|
||||||
import kotlin.properties.PropertyDelegateProvider
|
import kotlin.properties.PropertyDelegateProvider
|
||||||
@ -14,28 +13,37 @@ import kotlin.reflect.KProperty
|
|||||||
import kotlin.reflect.KProperty1
|
import kotlin.reflect.KProperty1
|
||||||
|
|
||||||
@OptIn(InternalDeviceAPI::class)
|
@OptIn(InternalDeviceAPI::class)
|
||||||
public abstract class DeviceSpec<D : DeviceBySpec<D>>(
|
public abstract class DeviceSpec<D : Device> {
|
||||||
private val buildDevice: () -> D
|
//initializing meta property for everyone
|
||||||
) : Factory<D> {
|
private val _properties = hashMapOf<String, DevicePropertySpec<D, *>>(
|
||||||
private val _properties = HashMap<String, DevicePropertySpec<D, *>>()
|
DeviceMetaPropertySpec.name to DeviceMetaPropertySpec
|
||||||
|
)
|
||||||
public val properties: Map<String, DevicePropertySpec<D, *>> get() = _properties
|
public val properties: Map<String, DevicePropertySpec<D, *>> get() = _properties
|
||||||
|
|
||||||
private val _actions = HashMap<String, DeviceActionSpec<D, *, *>>()
|
private val _actions = HashMap<String, DeviceActionSpec<D, *, *>>()
|
||||||
public val actions: Map<String, DeviceActionSpec<D, *, *>> get() = _actions
|
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
|
_properties[deviceProperty.name] = deviceProperty
|
||||||
return deviceProperty
|
return deviceProperty
|
||||||
}
|
}
|
||||||
|
|
||||||
public fun <T : Any> registerProperty(
|
public fun <T> registerProperty(
|
||||||
converter: MetaConverter<T>,
|
converter: MetaConverter<T>,
|
||||||
readOnlyProperty: KProperty1<D, T>,
|
readOnlyProperty: KProperty1<D, T>,
|
||||||
descriptorBuilder: PropertyDescriptor.() -> Unit = {}
|
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
||||||
): DevicePropertySpec<D, T> {
|
): DevicePropertySpec<D, T> {
|
||||||
val deviceProperty = object : DevicePropertySpec<D, T> {
|
val deviceProperty = object : DevicePropertySpec<D, T> {
|
||||||
override val name: String = readOnlyProperty.name
|
override val descriptor: PropertyDescriptor =
|
||||||
override val descriptor: PropertyDescriptor = PropertyDescriptor(this.name).apply(descriptorBuilder)
|
PropertyDescriptor(readOnlyProperty.name).apply(descriptorBuilder)
|
||||||
override val converter: MetaConverter<T> = converter
|
override val converter: MetaConverter<T> = converter
|
||||||
override suspend fun read(device: D): T =
|
override suspend fun read(device: D): T =
|
||||||
withContext(device.coroutineContext) { readOnlyProperty.get(device) }
|
withContext(device.coroutineContext) { readOnlyProperty.get(device) }
|
||||||
@ -43,16 +51,39 @@ public abstract class DeviceSpec<D : DeviceBySpec<D>>(
|
|||||||
return registerProperty(deviceProperty)
|
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>,
|
converter: MetaConverter<T>,
|
||||||
readWriteProperty: KMutableProperty1<D, T>,
|
readWriteProperty: KMutableProperty1<D, T>,
|
||||||
descriptorBuilder: PropertyDescriptor.() -> Unit = {}
|
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
||||||
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<Any?, WritableDevicePropertySpec<D, T>>> =
|
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<Any?, WritableDevicePropertySpec<D, T>>> =
|
||||||
PropertyDelegateProvider { _, property ->
|
PropertyDelegateProvider { _, property ->
|
||||||
val deviceProperty = object : WritableDevicePropertySpec<D, T> {
|
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
|
//TODO add type from converter
|
||||||
writable = true
|
writable = true
|
||||||
}.apply(descriptorBuilder)
|
}.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>,
|
converter: MetaConverter<T>,
|
||||||
name: String? = null,
|
|
||||||
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
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>, ReadOnlyProperty<DeviceSpec<D>, DevicePropertySpec<D, T>>> =
|
||||||
PropertyDelegateProvider { _: DeviceSpec<D>, property ->
|
PropertyDelegateProvider { _: DeviceSpec<D>, property ->
|
||||||
val propertyName = name ?: property.name
|
val propertyName = name ?: property.name
|
||||||
val deviceProperty = object : DevicePropertySpec<D, T> {
|
val deviceProperty = object : DevicePropertySpec<D, T> {
|
||||||
override val name: String = propertyName
|
override val descriptor: PropertyDescriptor = PropertyDescriptor(propertyName).apply(descriptorBuilder)
|
||||||
override val descriptor: PropertyDescriptor = PropertyDescriptor(this.name).apply(descriptorBuilder)
|
|
||||||
override val converter: MetaConverter<T> = converter
|
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)
|
registerProperty(deviceProperty)
|
||||||
ReadOnlyProperty<DeviceSpec<D>, DevicePropertySpec<D, T>> { _, _ ->
|
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>,
|
converter: MetaConverter<T>,
|
||||||
name: String? = null,
|
|
||||||
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
||||||
read: suspend D.() -> T,
|
name: String? = null,
|
||||||
write: suspend D.(T) -> Unit
|
read: suspend D.() -> T?,
|
||||||
|
write: suspend D.(T) -> Unit,
|
||||||
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, WritableDevicePropertySpec<D, T>>> =
|
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, WritableDevicePropertySpec<D, T>>> =
|
||||||
PropertyDelegateProvider { _: DeviceSpec<D>, property: KProperty<*> ->
|
PropertyDelegateProvider { _: DeviceSpec<D>, property: KProperty<*> ->
|
||||||
val propertyName = name ?: property.name
|
val propertyName = name ?: property.name
|
||||||
val deviceProperty = object : WritableDevicePropertySpec<D, T> {
|
val deviceProperty = object : WritableDevicePropertySpec<D, T> {
|
||||||
override val name: String = propertyName
|
override val descriptor: PropertyDescriptor = PropertyDescriptor(propertyName).apply(descriptorBuilder)
|
||||||
override val descriptor: PropertyDescriptor = PropertyDescriptor(this.name).apply(descriptorBuilder)
|
|
||||||
override val converter: MetaConverter<T> = converter
|
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) {
|
override suspend fun write(device: D, value: T): Unit = withContext(device.coroutineContext) {
|
||||||
device.write(value)
|
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
|
_actions[deviceAction.name] = deviceAction
|
||||||
return deviceAction
|
return deviceAction
|
||||||
}
|
}
|
||||||
|
|
||||||
public fun <I : Any, O : Any> action(
|
public fun <I, O> action(
|
||||||
inputConverter: MetaConverter<I>,
|
inputConverter: MetaConverter<I>,
|
||||||
outputConverter: MetaConverter<O>,
|
outputConverter: MetaConverter<O>,
|
||||||
name: String? = null,
|
|
||||||
descriptorBuilder: ActionDescriptor.() -> Unit = {},
|
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>, ReadOnlyProperty<DeviceSpec<D>, DeviceActionSpec<D, I, O>>> =
|
||||||
PropertyDelegateProvider { _: DeviceSpec<D>, property ->
|
PropertyDelegateProvider { _: DeviceSpec<D>, property ->
|
||||||
val actionName = name ?: property.name
|
val actionName = name ?: property.name
|
||||||
val deviceAction = object : DeviceActionSpec<D, I, O> {
|
val deviceAction = object : DeviceActionSpec<D, I, O> {
|
||||||
override val name: String = actionName
|
|
||||||
override val descriptor: ActionDescriptor = ActionDescriptor(actionName).apply(descriptorBuilder)
|
override val descriptor: ActionDescriptor = ActionDescriptor(actionName).apply(descriptorBuilder)
|
||||||
|
|
||||||
override val inputConverter: MetaConverter<I> = inputConverter
|
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() {}
|
public fun unitAction(
|
||||||
|
descriptorBuilder: ActionDescriptor.() -> Unit = {},
|
||||||
|
name: String? = null,
|
||||||
override fun invoke(meta: Meta, context: Context): D = buildDevice().apply {
|
execute: suspend D.() -> Unit,
|
||||||
this.context = context
|
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DeviceActionSpec<D, Meta, Meta>>> =
|
||||||
this.meta = meta
|
action(
|
||||||
onStartup()
|
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.Job
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
@ -14,7 +14,7 @@ import kotlin.time.Duration
|
|||||||
*
|
*
|
||||||
* The flow is canceled when the device scope is canceled
|
* 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) {
|
while (isActive) {
|
||||||
kotlinx.coroutines.delay(interval)
|
kotlinx.coroutines.delay(interval)
|
||||||
emit(reader())
|
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) {
|
while (isActive) {
|
||||||
kotlinx.coroutines.delay(interval)
|
kotlinx.coroutines.delay(interval)
|
||||||
task()
|
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.*
|
||||||
import space.kscience.dataforge.meta.double
|
|
||||||
import space.kscience.dataforge.meta.enum
|
|
||||||
import space.kscience.dataforge.meta.get
|
|
||||||
import space.kscience.dataforge.meta.transformations.MetaConverter
|
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.Duration
|
||||||
import kotlin.time.DurationUnit
|
import kotlin.time.DurationUnit
|
||||||
import kotlin.time.toDuration
|
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 space.kscience.controls.api.PropertyDescriptor
|
||||||
import ru.mipt.npm.controls.api.metaDescriptor
|
import space.kscience.controls.api.metaDescriptor
|
||||||
import space.kscience.dataforge.meta.Meta
|
import space.kscience.dataforge.meta.Meta
|
||||||
|
import space.kscience.dataforge.meta.ValueType
|
||||||
import space.kscience.dataforge.meta.transformations.MetaConverter
|
import space.kscience.dataforge.meta.transformations.MetaConverter
|
||||||
import space.kscience.dataforge.values.ValueType
|
|
||||||
import kotlin.properties.PropertyDelegateProvider
|
import kotlin.properties.PropertyDelegateProvider
|
||||||
import kotlin.properties.ReadOnlyProperty
|
import kotlin.properties.ReadOnlyProperty
|
||||||
|
|
||||||
//read only delegates
|
//read only delegates
|
||||||
|
|
||||||
public fun <D : DeviceBySpec<D>> DeviceSpec<D>.booleanProperty(
|
public fun <D : DeviceBase<D>> DeviceSpec<D>.booleanProperty(
|
||||||
name: String? = null,
|
|
||||||
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
||||||
read: suspend D.() -> Boolean
|
name: String? = null,
|
||||||
|
read: suspend D.() -> Boolean?
|
||||||
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DevicePropertySpec<D, Boolean>>> = property(
|
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DevicePropertySpec<D, Boolean>>> = property(
|
||||||
MetaConverter.boolean,
|
MetaConverter.boolean,
|
||||||
name,
|
|
||||||
{
|
{
|
||||||
metaDescriptor {
|
metaDescriptor {
|
||||||
type(ValueType.BOOLEAN)
|
type(ValueType.BOOLEAN)
|
||||||
}
|
}
|
||||||
descriptorBuilder()
|
descriptorBuilder()
|
||||||
},
|
},
|
||||||
|
name,
|
||||||
read
|
read
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -35,110 +35,110 @@ private inline fun numberDescriptor(
|
|||||||
descriptorBuilder()
|
descriptorBuilder()
|
||||||
}
|
}
|
||||||
|
|
||||||
public fun <D : DeviceBySpec<D>> DeviceSpec<D>.numberProperty(
|
public fun <D : DeviceBase<D>> DeviceSpec<D>.numberProperty(
|
||||||
name: String? = null,
|
name: String? = null,
|
||||||
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
||||||
read: suspend D.() -> Number
|
read: suspend D.() -> Number?
|
||||||
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DevicePropertySpec<D, Number>>> = property(
|
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DevicePropertySpec<D, Number>>> = property(
|
||||||
MetaConverter.number,
|
MetaConverter.number,
|
||||||
name,
|
|
||||||
numberDescriptor(descriptorBuilder),
|
numberDescriptor(descriptorBuilder),
|
||||||
|
name,
|
||||||
read
|
read
|
||||||
)
|
)
|
||||||
|
|
||||||
public fun <D : DeviceBySpec<D>> DeviceSpec<D>.doubleProperty(
|
public fun <D : DeviceBase<D>> DeviceSpec<D>.doubleProperty(
|
||||||
name: String? = null,
|
|
||||||
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
||||||
read: suspend D.() -> Double
|
name: String? = null,
|
||||||
|
read: suspend D.() -> Double?
|
||||||
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DevicePropertySpec<D, Double>>> = property(
|
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DevicePropertySpec<D, Double>>> = property(
|
||||||
MetaConverter.double,
|
MetaConverter.double,
|
||||||
name,
|
|
||||||
numberDescriptor(descriptorBuilder),
|
numberDescriptor(descriptorBuilder),
|
||||||
|
name,
|
||||||
read
|
read
|
||||||
)
|
)
|
||||||
|
|
||||||
public fun <D : DeviceBySpec<D>> DeviceSpec<D>.stringProperty(
|
public fun <D : DeviceBase<D>> DeviceSpec<D>.stringProperty(
|
||||||
name: String? = null,
|
|
||||||
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
||||||
read: suspend D.() -> String
|
name: String? = null,
|
||||||
|
read: suspend D.() -> String?
|
||||||
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DevicePropertySpec<D, String>>> = property(
|
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DevicePropertySpec<D, String>>> = property(
|
||||||
MetaConverter.string,
|
MetaConverter.string,
|
||||||
name,
|
|
||||||
{
|
{
|
||||||
metaDescriptor {
|
metaDescriptor {
|
||||||
type(ValueType.STRING)
|
type(ValueType.STRING)
|
||||||
}
|
}
|
||||||
descriptorBuilder()
|
descriptorBuilder()
|
||||||
},
|
},
|
||||||
|
name,
|
||||||
read
|
read
|
||||||
)
|
)
|
||||||
|
|
||||||
public fun <D : DeviceBySpec<D>> DeviceSpec<D>.metaProperty(
|
public fun <D : DeviceBase<D>> DeviceSpec<D>.metaProperty(
|
||||||
name: String? = null,
|
|
||||||
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
||||||
read: suspend D.() -> Meta
|
name: String? = null,
|
||||||
|
read: suspend D.() -> Meta?
|
||||||
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DevicePropertySpec<D, Meta>>> = property(
|
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, DevicePropertySpec<D, Meta>>> = property(
|
||||||
MetaConverter.meta,
|
MetaConverter.meta,
|
||||||
name,
|
|
||||||
{
|
{
|
||||||
metaDescriptor {
|
metaDescriptor {
|
||||||
type(ValueType.STRING)
|
type(ValueType.STRING)
|
||||||
}
|
}
|
||||||
descriptorBuilder()
|
descriptorBuilder()
|
||||||
},
|
},
|
||||||
|
name,
|
||||||
read
|
read
|
||||||
)
|
)
|
||||||
|
|
||||||
//read-write delegates
|
//read-write delegates
|
||||||
|
|
||||||
public fun <D : DeviceBySpec<D>> DeviceSpec<D>.booleanProperty(
|
public fun <D : DeviceBase<D>> DeviceSpec<D>.booleanProperty(
|
||||||
name: String? = null,
|
|
||||||
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
||||||
read: suspend D.() -> Boolean,
|
name: String? = null,
|
||||||
|
read: suspend D.() -> Boolean?,
|
||||||
write: suspend D.(Boolean) -> Unit
|
write: suspend D.(Boolean) -> Unit
|
||||||
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, WritableDevicePropertySpec<D, Boolean>>> =
|
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, WritableDevicePropertySpec<D, Boolean>>> =
|
||||||
property(
|
mutableProperty(
|
||||||
MetaConverter.boolean,
|
MetaConverter.boolean,
|
||||||
name,
|
|
||||||
{
|
{
|
||||||
metaDescriptor {
|
metaDescriptor {
|
||||||
type(ValueType.BOOLEAN)
|
type(ValueType.BOOLEAN)
|
||||||
}
|
}
|
||||||
descriptorBuilder()
|
descriptorBuilder()
|
||||||
},
|
},
|
||||||
|
name,
|
||||||
read,
|
read,
|
||||||
write
|
write
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
public fun <D : DeviceBySpec<D>> DeviceSpec<D>.numberProperty(
|
public fun <D : DeviceBase<D>> DeviceSpec<D>.numberProperty(
|
||||||
name: String? = null,
|
|
||||||
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
||||||
|
name: String? = null,
|
||||||
read: suspend D.() -> Number,
|
read: suspend D.() -> Number,
|
||||||
write: suspend D.(Number) -> Unit
|
write: suspend D.(Number) -> Unit
|
||||||
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, WritableDevicePropertySpec<D, Number>>> =
|
): 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(
|
public fun <D : DeviceBase<D>> DeviceSpec<D>.doubleProperty(
|
||||||
name: String? = null,
|
|
||||||
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
||||||
|
name: String? = null,
|
||||||
read: suspend D.() -> Double,
|
read: suspend D.() -> Double,
|
||||||
write: suspend D.(Double) -> Unit
|
write: suspend D.(Double) -> Unit
|
||||||
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, WritableDevicePropertySpec<D, Double>>> =
|
): 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(
|
public fun <D : DeviceBase<D>> DeviceSpec<D>.stringProperty(
|
||||||
name: String? = null,
|
|
||||||
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
||||||
|
name: String? = null,
|
||||||
read: suspend D.() -> String,
|
read: suspend D.() -> String,
|
||||||
write: suspend D.(String) -> Unit
|
write: suspend D.(String) -> Unit
|
||||||
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, WritableDevicePropertySpec<D, String>>> =
|
): 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(
|
public fun <D : DeviceBase<D>> DeviceSpec<D>.metaProperty(
|
||||||
name: String? = null,
|
|
||||||
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
|
||||||
|
name: String? = null,
|
||||||
read: suspend D.() -> Meta,
|
read: suspend D.() -> Meta,
|
||||||
write: suspend D.(Meta) -> Unit
|
write: suspend D.(Meta) -> Unit
|
||||||
): PropertyDelegateProvider<DeviceSpec<D>, ReadOnlyProperty<DeviceSpec<D>, WritableDevicePropertySpec<D, Meta>>> =
|
): 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.Meta
|
||||||
import space.kscience.dataforge.meta.get
|
import space.kscience.dataforge.meta.get
|
||||||
import space.kscience.dataforge.meta.long
|
import space.kscience.dataforge.meta.long
|
||||||
import space.kscience.dataforge.values.long
|
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
|
|
||||||
// TODO move to core
|
// TODO move to core
|
@ -1,4 +1,4 @@
|
|||||||
package ru.mipt.npm.controls.ports
|
package space.kscience.controls.ports
|
||||||
|
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import space.kscience.dataforge.context.Context
|
import space.kscience.dataforge.context.Context
|
||||||
@ -58,10 +58,11 @@ public class TcpPort private constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun write(data: ByteArray) {
|
override suspend fun write(data: ByteArray): Unit = withContext(Dispatchers.IO){
|
||||||
futureChannel.await().write(ByteBuffer.wrap(data))
|
futureChannel.await().write(ByteBuffer.wrap(data))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
override fun close() {
|
override fun close() {
|
||||||
listenerJob.cancel()
|
listenerJob.cancel()
|
||||||
if (futureChannel.isCompleted) {
|
if (futureChannel.isCompleted) {
|
||||||
@ -73,6 +74,9 @@ public class TcpPort private constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
public companion object : PortFactory {
|
public companion object : PortFactory {
|
||||||
|
|
||||||
|
override val type: String = "tcp"
|
||||||
|
|
||||||
public fun open(
|
public fun open(
|
||||||
context: Context,
|
context: Context,
|
||||||
host: String,
|
host: String,
|
||||||
@ -82,7 +86,7 @@ public class TcpPort private constructor(
|
|||||||
return TcpPort(context, host, port, coroutineContext)
|
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 host = meta["host"].string ?: "localhost"
|
||||||
val port = meta["port"].int ?: error("Port value for TCP port is not defined in $meta")
|
val port = meta["port"].int ?: error("Port value for TCP port is not defined in $meta")
|
||||||
return open(context, host, port)
|
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.flowOf
|
||||||
import kotlinx.coroutines.flow.map
|
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.selector.ActorSelectorManager
|
||||||
import io.ktor.network.sockets.aSocket
|
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.get
|
||||||
import space.kscience.dataforge.meta.int
|
import space.kscience.dataforge.meta.int
|
||||||
import space.kscience.dataforge.meta.string
|
import space.kscience.dataforge.meta.string
|
||||||
import java.net.InetSocketAddress
|
|
||||||
import kotlin.coroutines.CoroutineContext
|
import kotlin.coroutines.CoroutineContext
|
||||||
|
|
||||||
public class KtorTcpPort internal constructor(
|
public class KtorTcpPort internal constructor(
|
||||||
@ -29,7 +28,7 @@ public class KtorTcpPort internal constructor(
|
|||||||
override fun toString(): String = "port[tcp:$host:$port]"
|
override fun toString(): String = "port[tcp:$host:$port]"
|
||||||
|
|
||||||
private val futureSocket = scope.async {
|
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 {
|
private val writeChannel = scope.async {
|
||||||
@ -38,7 +37,7 @@ public class KtorTcpPort internal constructor(
|
|||||||
|
|
||||||
private val listenerJob = scope.launch {
|
private val listenerJob = scope.launch {
|
||||||
val input = futureSocket.await().openReadChannel()
|
val input = futureSocket.await().openReadChannel()
|
||||||
input.consumeEachBufferRange { buffer, last ->
|
input.consumeEachBufferRange { buffer, _ ->
|
||||||
val array = ByteArray(buffer.remaining())
|
val array = ByteArray(buffer.remaining())
|
||||||
buffer.get(array)
|
buffer.get(array)
|
||||||
receive(array)
|
receive(array)
|
||||||
@ -57,6 +56,9 @@ public class KtorTcpPort internal constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
public companion object : PortFactory {
|
public companion object : PortFactory {
|
||||||
|
|
||||||
|
override val type: String = "tcp"
|
||||||
|
|
||||||
public fun open(
|
public fun open(
|
||||||
context: Context,
|
context: Context,
|
||||||
host: String,
|
host: String,
|
||||||
@ -66,7 +68,7 @@ public class KtorTcpPort internal constructor(
|
|||||||
return KtorTcpPort(context, host, port, coroutineContext)
|
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 host = meta["host"].string ?: "localhost"
|
||||||
val port = meta["port"].int ?: error("Port value for TCP port is not defined in $meta")
|
val port = meta["port"].int ?: error("Port value for TCP port is not defined in $meta")
|
||||||
return open(context, host, port)
|
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 {
|
plugins {
|
||||||
id("ru.mipt.npm.gradle.mpp")
|
id("space.kscience.gradle.mpp")
|
||||||
`maven-publish`
|
`maven-publish`
|
||||||
}
|
}
|
||||||
|
|
||||||
kscience{
|
kscience{
|
||||||
|
jvm()
|
||||||
|
js()
|
||||||
useSerialization {
|
useSerialization {
|
||||||
json()
|
json()
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
kotlin {
|
|
||||||
sourceSets {
|
|
||||||
commonMain {
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(project(":magix:magix-rsocket"))
|
implementation(project(":magix:magix-rsocket"))
|
||||||
implementation(project(":controls-core"))
|
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.SerialName
|
||||||
import kotlinx.serialization.Serializable
|
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.Job
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import ru.mipt.npm.controls.api.get
|
import space.kscience.controls.api.get
|
||||||
import ru.mipt.npm.controls.api.getOrReadProperty
|
import space.kscience.controls.api.getOrReadProperty
|
||||||
import ru.mipt.npm.controls.controllers.DeviceManager
|
import space.kscience.controls.manager.DeviceManager
|
||||||
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.error
|
||||||
import space.kscience.dataforge.context.logger
|
import space.kscience.dataforge.context.logger
|
||||||
import space.kscience.dataforge.meta.Meta
|
import space.kscience.dataforge.meta.Meta
|
||||||
|
import space.kscience.magix.api.*
|
||||||
|
|
||||||
public const val TANGO_MAGIX_FORMAT: String = "tango"
|
public const val TANGO_MAGIX_FORMAT: String = "tango"
|
||||||
|
|
||||||
@ -59,33 +58,39 @@ public data class TangoPayload(
|
|||||||
val argin: Meta? = null,
|
val argin: Meta? = null,
|
||||||
val argout: Meta? = null,
|
val argout: Meta? = null,
|
||||||
val data: 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(
|
public fun DeviceManager.launchTangoMagix(
|
||||||
endpoint: MagixEndpoint<TangoPayload>,
|
endpoint: MagixEndpoint,
|
||||||
endpointID: String = TANGO_MAGIX_FORMAT,
|
endpointID: String = TANGO_MAGIX_FORMAT,
|
||||||
): Job {
|
): Job {
|
||||||
suspend fun respond(request: MagixMessage<TangoPayload>, payloadBuilder: (TangoPayload) -> TangoPayload) {
|
|
||||||
|
suspend fun respond(request: MagixMessage, payload: TangoPayload, payloadBuilder: (TangoPayload) -> TangoPayload) {
|
||||||
endpoint.broadcast(
|
endpoint.broadcast(
|
||||||
request.copy(
|
tangoMagixFormat,
|
||||||
id = generateId(request),
|
id = generateId(request),
|
||||||
parentId = request.id,
|
parentId = request.id,
|
||||||
origin = endpointID,
|
origin = endpointID,
|
||||||
payload = payloadBuilder(request.payload)
|
payload = payloadBuilder(payload)
|
||||||
)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
return context.launch {
|
return context.launch {
|
||||||
endpoint.subscribe().onEach { request ->
|
endpoint.subscribe(tangoMagixFormat).onEach { (request, payload) ->
|
||||||
try {
|
try {
|
||||||
val device = get(request.payload.device)
|
val device = get(payload.device)
|
||||||
when (request.payload.action) {
|
when (payload.action) {
|
||||||
TangoAction.read -> {
|
TangoAction.read -> {
|
||||||
val value = device.getOrReadProperty(request.payload.name)
|
val value = device.getOrReadProperty(payload.name)
|
||||||
respond(request) { requestPayload ->
|
respond(request, payload) { requestPayload ->
|
||||||
requestPayload.copy(
|
requestPayload.copy(
|
||||||
value = value,
|
value = value,
|
||||||
quality = TangoQuality.VALID
|
quality = TangoQuality.VALID
|
||||||
@ -93,12 +98,12 @@ public fun DeviceManager.launchTangoMagix(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
TangoAction.write -> {
|
TangoAction.write -> {
|
||||||
request.payload.value?.let { value ->
|
payload.value?.let { value ->
|
||||||
device.writeProperty(request.payload.name, value)
|
device.writeProperty(payload.name, value)
|
||||||
}
|
}
|
||||||
//wait for value to be written and return final state
|
//wait for value to be written and return final state
|
||||||
val value = device.getOrReadProperty(request.payload.name)
|
val value = device.getOrReadProperty(payload.name)
|
||||||
respond(request) { requestPayload ->
|
respond(request, payload) { requestPayload ->
|
||||||
requestPayload.copy(
|
requestPayload.copy(
|
||||||
value = value,
|
value = value,
|
||||||
quality = TangoQuality.VALID
|
quality = TangoQuality.VALID
|
||||||
@ -106,8 +111,8 @@ public fun DeviceManager.launchTangoMagix(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
TangoAction.exec -> {
|
TangoAction.exec -> {
|
||||||
val result = device.execute(request.payload.name, request.payload.argin)
|
val result = device.execute(payload.name, payload.argin)
|
||||||
respond(request) { requestPayload ->
|
respond(request, payload) { requestPayload ->
|
||||||
requestPayload.copy(
|
requestPayload.copy(
|
||||||
argout = result,
|
argout = result,
|
||||||
quality = TangoQuality.VALID
|
quality = TangoQuality.VALID
|
||||||
@ -119,12 +124,11 @@ public fun DeviceManager.launchTangoMagix(
|
|||||||
} catch (ex: Exception) {
|
} catch (ex: Exception) {
|
||||||
logger.error(ex) { "Error while responding to message" }
|
logger.error(ex) { "Error while responding to message" }
|
||||||
endpoint.broadcast(
|
endpoint.broadcast(
|
||||||
request.copy(
|
tangoMagixFormat,
|
||||||
id = generateId(request),
|
id = generateId(request),
|
||||||
parentId = request.id,
|
parentId = request.id,
|
||||||
origin = endpointID,
|
origin = endpointID,
|
||||||
payload = request.payload.copy(quality = TangoQuality.WARNING)
|
payload = payload.copy(quality = TangoQuality.WARNING)
|
||||||
)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}.launchIn(this)
|
}.launchIn(this)
|
@ -1,14 +1,14 @@
|
|||||||
plugins {
|
plugins {
|
||||||
id("ru.mipt.npm.gradle.jvm")
|
id("space.kscience.gradle.jvm")
|
||||||
}
|
}
|
||||||
|
|
||||||
val ktorVersion: String by rootProject.extra
|
val ktorVersion: String by rootProject.extra
|
||||||
|
|
||||||
val miloVersion: String = "0.6.3"
|
val miloVersion: String = "0.6.7"
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
api(project(":controls-core"))
|
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:sdk-client:$miloVersion")
|
||||||
api("org.eclipse.milo:bsd-parser:$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.AbstractCodec
|
||||||
import org.eclipse.milo.opcua.binaryschema.parser.BsdParser
|
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.eclipse.milo.opcua.stack.core.types.builtin.unsigned.*
|
||||||
import org.opcfoundation.opcua.binaryschema.EnumeratedType
|
import org.opcfoundation.opcua.binaryschema.EnumeratedType
|
||||||
import org.opcfoundation.opcua.binaryschema.StructuredType
|
import org.opcfoundation.opcua.binaryschema.StructuredType
|
||||||
import ru.mipt.npm.controls.misc.instant
|
import space.kscience.controls.misc.instant
|
||||||
import ru.mipt.npm.controls.misc.toMeta
|
import space.kscience.controls.misc.toMeta
|
||||||
import space.kscience.dataforge.meta.*
|
import space.kscience.dataforge.meta.*
|
||||||
import space.kscience.dataforge.names.Name
|
import space.kscience.dataforge.names.Name
|
||||||
import space.kscience.dataforge.names.asName
|
import space.kscience.dataforge.names.asName
|
||||||
import space.kscience.dataforge.values.*
|
|
||||||
import java.util.*
|
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.coroutines.future.await
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import org.eclipse.milo.opcua.sdk.client.OpcUaClient
|
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.builtin.*
|
||||||
import org.eclipse.milo.opcua.stack.core.types.enumerated.TimestampsToReturn
|
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.Meta
|
||||||
import space.kscience.dataforge.meta.MetaSerializer
|
import space.kscience.dataforge.meta.MetaSerializer
|
||||||
import space.kscience.dataforge.meta.transformations.MetaConverter
|
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 time = data.serverTime ?: error("No server time provided")
|
||||||
val meta: Meta = when (val content = data.value.value) {
|
val meta: Meta = when (val content = data.value.value) {
|
||||||
is T -> return content to time
|
is T -> return content to time
|
||||||
content is Meta -> content as Meta
|
is Meta -> content
|
||||||
content is ExtensionObject -> (content as ExtensionObject).decode(client.dynamicSerializationContext) as Meta
|
is ExtensionObject -> content.decode(client.dynamicSerializationContext) as Meta
|
||||||
else -> error("Incompatible OPC property value $content")
|
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.launch
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import org.eclipse.milo.opcua.sdk.client.OpcUaClient
|
import org.eclipse.milo.opcua.sdk.client.OpcUaClient
|
||||||
import org.eclipse.milo.opcua.stack.core.types.builtin.NodeId
|
import org.eclipse.milo.opcua.stack.core.types.builtin.NodeId
|
||||||
import ru.mipt.npm.controls.properties.DeviceBySpec
|
import space.kscience.controls.spec.DeviceBySpec
|
||||||
import ru.mipt.npm.controls.properties.DeviceSpec
|
import space.kscience.controls.spec.DeviceSpec
|
||||||
import space.kscience.dataforge.context.Context
|
import space.kscience.dataforge.context.Context
|
||||||
import space.kscience.dataforge.context.Global
|
import space.kscience.dataforge.context.Global
|
||||||
import space.kscience.dataforge.meta.Meta
|
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.OpcUaClient
|
||||||
import org.eclipse.milo.opcua.sdk.client.api.config.OpcUaClientConfigBuilder
|
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.coroutines.launch
|
||||||
import kotlinx.datetime.toJavaInstant
|
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.Identifiers
|
||||||
import org.eclipse.milo.opcua.stack.core.types.builtin.DateTime
|
import org.eclipse.milo.opcua.stack.core.types.builtin.DateTime
|
||||||
import org.eclipse.milo.opcua.stack.core.types.builtin.LocalizedText
|
import org.eclipse.milo.opcua.stack.core.types.builtin.LocalizedText
|
||||||
import ru.mipt.npm.controls.api.Device
|
import space.kscience.controls.api.Device
|
||||||
import ru.mipt.npm.controls.api.DeviceHub
|
import space.kscience.controls.api.DeviceHub
|
||||||
import ru.mipt.npm.controls.api.PropertyDescriptor
|
import space.kscience.controls.api.PropertyDescriptor
|
||||||
import ru.mipt.npm.controls.api.onPropertyChange
|
import space.kscience.controls.api.onPropertyChange
|
||||||
import ru.mipt.npm.controls.controllers.DeviceManager
|
import space.kscience.controls.manager.DeviceManager
|
||||||
import space.kscience.dataforge.meta.Meta
|
import space.kscience.dataforge.meta.Meta
|
||||||
import space.kscience.dataforge.meta.MetaSerializer
|
import space.kscience.dataforge.meta.MetaSerializer
|
||||||
|
import space.kscience.dataforge.meta.ValueType
|
||||||
import space.kscience.dataforge.names.Name
|
import space.kscience.dataforge.names.Name
|
||||||
import space.kscience.dataforge.names.asName
|
import space.kscience.dataforge.names.asName
|
||||||
import space.kscience.dataforge.names.plus
|
import space.kscience.dataforge.names.plus
|
||||||
import space.kscience.dataforge.values.ValueType
|
|
||||||
|
|
||||||
|
|
||||||
public operator fun Device.get(propertyDescriptor: PropertyDescriptor): Meta? = getProperty(propertyDescriptor.name)
|
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 =
|
public fun OpcUaServer.serveDevices(deviceManager: DeviceManager): DeviceNameSpace =
|
||||||
DeviceNameSpace(this, deviceManager).apply { startup() }
|
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 kotlinx.serialization.json.Json
|
||||||
import org.eclipse.milo.opcua.stack.core.types.builtin.DataValue
|
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.DateTime
|
||||||
import org.eclipse.milo.opcua.stack.core.types.builtin.StatusCode
|
import org.eclipse.milo.opcua.stack.core.types.builtin.StatusCode
|
||||||
import org.eclipse.milo.opcua.stack.core.types.builtin.Variant
|
import org.eclipse.milo.opcua.stack.core.types.builtin.Variant
|
||||||
import space.kscience.dataforge.meta.Meta
|
import space.kscience.dataforge.meta.*
|
||||||
import space.kscience.dataforge.meta.MetaSerializer
|
|
||||||
import space.kscience.dataforge.meta.isLeaf
|
|
||||||
import space.kscience.dataforge.values.*
|
|
||||||
import java.time.Instant
|
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.AccessLevel
|
||||||
import org.eclipse.milo.opcua.sdk.core.Reference
|
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.OpcUaServer
|
||||||
import org.eclipse.milo.opcua.sdk.server.api.config.OpcUaServerConfig
|
import org.eclipse.milo.opcua.sdk.server.api.config.OpcUaServerConfig
|
@ -1,5 +1,5 @@
|
|||||||
plugins {
|
plugins {
|
||||||
id("ru.mipt.npm.gradle.jvm")
|
id("space.kscience.gradle.jvm")
|
||||||
`maven-publish`
|
`maven-publish`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
package ru.mipt.npm.controls.serial
|
package space.kscience.controls.serial
|
||||||
|
|
||||||
import jssc.SerialPort.*
|
import jssc.SerialPort.*
|
||||||
import jssc.SerialPortEventListener
|
import jssc.SerialPortEventListener
|
||||||
import ru.mipt.npm.controls.ports.AbstractPort
|
import space.kscience.controls.ports.AbstractPort
|
||||||
import ru.mipt.npm.controls.ports.Port
|
import space.kscience.controls.ports.Port
|
||||||
import ru.mipt.npm.controls.ports.PortFactory
|
import space.kscience.controls.ports.PortFactory
|
||||||
import space.kscience.dataforge.context.Context
|
import space.kscience.dataforge.context.Context
|
||||||
import space.kscience.dataforge.meta.Meta
|
import space.kscience.dataforge.meta.Meta
|
||||||
import space.kscience.dataforge.meta.int
|
import space.kscience.dataforge.meta.int
|
||||||
@ -58,6 +58,9 @@ public class SerialPort private constructor(
|
|||||||
|
|
||||||
public companion object : PortFactory {
|
public companion object : PortFactory {
|
||||||
|
|
||||||
|
override val type: String = "com"
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Construct ComPort with given parameters
|
* Construct ComPort with given parameters
|
||||||
*/
|
*/
|
||||||
@ -77,7 +80,7 @@ public class SerialPort private constructor(
|
|||||||
return SerialPort(context, jssc, coroutineContext)
|
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 name by meta.string { error("Serial port name not defined") }
|
||||||
val baudRate by meta.int(BAUDRATE_9600)
|
val baudRate by meta.int(BAUDRATE_9600)
|
||||||
val dataBits by meta.int(DATABITS_8)
|
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 {
|
plugins {
|
||||||
id("ru.mipt.npm.gradle.jvm")
|
id("space.kscience.gradle.jvm")
|
||||||
`maven-publish`
|
`maven-publish`
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -12,10 +12,12 @@ val ktorVersion: String by rootProject.extra
|
|||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(project(":controls-core"))
|
implementation(project(":controls-core"))
|
||||||
implementation(project(":controls-tcp"))
|
implementation(project(":controls-ktor-tcp"))
|
||||||
implementation(projects.magix.magixServer)
|
implementation(projects.magix.magixServer)
|
||||||
implementation("io.ktor:ktor-server-cio:$ktorVersion")
|
implementation("io.ktor:ktor-server-cio:$ktorVersion")
|
||||||
implementation("io.ktor:ktor-websockets:$ktorVersion")
|
implementation("io.ktor:ktor-server-websockets:$ktorVersion")
|
||||||
implementation("io.ktor:ktor-serialization:$ktorVersion")
|
implementation("io.ktor:ktor-server-content-negotiation:$ktorVersion")
|
||||||
implementation("io.ktor:ktor-html-builder:$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.http.HttpStatusCode
|
||||||
import io.ktor.request.receiveText
|
import io.ktor.server.application.*
|
||||||
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.cio.CIO
|
import io.ktor.server.cio.CIO
|
||||||
import io.ktor.server.engine.ApplicationEngine
|
import io.ktor.server.engine.ApplicationEngine
|
||||||
import io.ktor.server.engine.embeddedServer
|
import io.ktor.server.engine.embeddedServer
|
||||||
import io.ktor.util.getValue
|
import io.ktor.server.html.respondHtml
|
||||||
import io.ktor.websocket.WebSockets
|
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.CoroutineScope
|
||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
import kotlinx.html.*
|
import kotlinx.html.*
|
||||||
@ -27,36 +26,29 @@ import kotlinx.serialization.json.Json
|
|||||||
import kotlinx.serialization.json.buildJsonArray
|
import kotlinx.serialization.json.buildJsonArray
|
||||||
import kotlinx.serialization.json.encodeToJsonElement
|
import kotlinx.serialization.json.encodeToJsonElement
|
||||||
import kotlinx.serialization.json.put
|
import kotlinx.serialization.json.put
|
||||||
import ru.mipt.npm.controls.api.DeviceMessage
|
import space.kscience.controls.api.DeviceMessage
|
||||||
import ru.mipt.npm.controls.api.PropertyGetMessage
|
import space.kscience.controls.api.PropertyGetMessage
|
||||||
import ru.mipt.npm.controls.api.PropertySetMessage
|
import space.kscience.controls.api.PropertySetMessage
|
||||||
import ru.mipt.npm.controls.api.getOrNull
|
import space.kscience.controls.api.getOrNull
|
||||||
import ru.mipt.npm.controls.controllers.DeviceManager
|
import space.kscience.controls.manager.DeviceManager
|
||||||
import ru.mipt.npm.controls.controllers.respondHubMessage
|
import space.kscience.controls.manager.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.dataforge.meta.toMeta
|
import space.kscience.dataforge.meta.toMeta
|
||||||
import space.kscience.dataforge.names.Name
|
import space.kscience.dataforge.names.Name
|
||||||
import space.kscience.dataforge.names.asName
|
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
|
||||||
|
|
||||||
/**
|
|
||||||
* Create and start a web server for several devices
|
|
||||||
*/
|
|
||||||
public fun CoroutineScope.startDeviceServer(
|
|
||||||
manager: DeviceManager,
|
|
||||||
port: Int = MagixEndpoint.DEFAULT_MAGIX_HTTP_PORT,
|
|
||||||
host: String = "localhost",
|
|
||||||
): ApplicationEngine {
|
|
||||||
|
|
||||||
return this.embeddedServer(CIO, port, host) {
|
|
||||||
|
private fun Application.deviceServerModule(manager: DeviceManager) {
|
||||||
install(WebSockets)
|
install(WebSockets)
|
||||||
install(CORS) {
|
// install(CORS) {
|
||||||
anyHost()
|
// anyHost()
|
||||||
}
|
// }
|
||||||
install(StatusPages) {
|
install(StatusPages) {
|
||||||
exception<IllegalArgumentException> { cause ->
|
exception<IllegalArgumentException> { call, cause ->
|
||||||
call.respond(HttpStatusCode.BadRequest, cause.message ?: "")
|
call.respond(HttpStatusCode.BadRequest, cause.message ?: "")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -66,9 +58,17 @@ public fun CoroutineScope.startDeviceServer(
|
|||||||
call.respondRedirect("/dashboard")
|
call.respondRedirect("/dashboard")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}.start()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create and start a web server for several devices
|
||||||
|
*/
|
||||||
|
public fun CoroutineScope.startDeviceServer(
|
||||||
|
manager: DeviceManager,
|
||||||
|
port: Int = MagixEndpoint.DEFAULT_MAGIX_HTTP_PORT,
|
||||||
|
host: String = "localhost",
|
||||||
|
): ApplicationEngine = embeddedServer(CIO, port, host, module = { deviceServerModule(manager) }).start()
|
||||||
|
|
||||||
public fun ApplicationEngine.whenStarted(callback: Application.() -> Unit) {
|
public fun ApplicationEngine.whenStarted(callback: Application.() -> Unit) {
|
||||||
environment.monitor.subscribe(ApplicationStarted, callback)
|
environment.monitor.subscribe(ApplicationStarted, callback)
|
||||||
}
|
}
|
||||||
@ -78,20 +78,20 @@ public val WEB_SERVER_TARGET: Name = "@webServer".asName()
|
|||||||
|
|
||||||
public fun Application.deviceManagerModule(
|
public fun Application.deviceManagerModule(
|
||||||
manager: DeviceManager,
|
manager: DeviceManager,
|
||||||
|
vararg plugins: MagixFlowPlugin,
|
||||||
deviceNames: Collection<String> = manager.devices.keys.map { it.toString() },
|
deviceNames: Collection<String> = manager.devices.keys.map { it.toString() },
|
||||||
route: String = "/",
|
route: String = "/",
|
||||||
rawSocketPort: Int = MagixEndpoint.DEFAULT_MAGIX_RAW_PORT,
|
|
||||||
buffer: Int = 100,
|
buffer: Int = 100,
|
||||||
) {
|
) {
|
||||||
if (featureOrNull(WebSockets) == null) {
|
if (pluginOrNull(WebSockets) == null) {
|
||||||
install(WebSockets)
|
install(WebSockets)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (featureOrNull(CORS) == null) {
|
// if (pluginOrNull(CORS) == null) {
|
||||||
install(CORS) {
|
// install(CORS) {
|
||||||
anyHost()
|
// anyHost()
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
routing {
|
routing {
|
||||||
route(route) {
|
route(route) {
|
||||||
@ -213,11 +213,13 @@ public fun Application.deviceManagerModule(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val magixFlow = MutableSharedFlow<GenericMagixMessage>(
|
val magixFlow = MutableSharedFlow<MagixMessage>(
|
||||||
buffer,
|
buffer,
|
||||||
extraBufferCapacity = buffer
|
extraBufferCapacity = buffer
|
||||||
)
|
)
|
||||||
|
|
||||||
launchMagixServerRawRSocket(magixFlow, rawSocketPort)
|
plugins.forEach {
|
||||||
|
it.start(this, magixFlow)
|
||||||
|
}
|
||||||
magixModule(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.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.JsonObjectBuilder
|
||||||
import kotlinx.serialization.json.buildJsonObject
|
import kotlinx.serialization.json.buildJsonObject
|
||||||
import ru.mipt.npm.controls.api.DeviceMessage
|
import space.kscience.controls.api.DeviceMessage
|
||||||
import ru.mipt.npm.magix.api.MagixEndpoint
|
import space.kscience.magix.api.MagixEndpoint
|
||||||
|
|
||||||
|
|
||||||
//internal fun Frame.toEnvelope(): Envelope {
|
//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,15 +1,13 @@
|
|||||||
plugins {
|
plugins {
|
||||||
kotlin("jvm")
|
kotlin("jvm")
|
||||||
id("org.openjfx.javafxplugin") version "0.0.9"
|
id("org.openjfx.javafxplugin") version "0.0.13"
|
||||||
application
|
application
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
jcenter()
|
|
||||||
maven("https://repo.kotlin.link")
|
maven("https://repo.kotlin.link")
|
||||||
maven("https://kotlin.bintray.com/kotlinx")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val ktorVersion: String by rootProject.extra
|
val ktorVersion: String by rootProject.extra
|
||||||
@ -26,23 +24,23 @@ dependencies{
|
|||||||
|
|
||||||
implementation("io.ktor:ktor-client-cio:$ktorVersion")
|
implementation("io.ktor:ktor-client-cio:$ktorVersion")
|
||||||
implementation("no.tornado:tornadofx:1.7.20")
|
implementation("no.tornado:tornadofx:1.7.20")
|
||||||
implementation("space.kscience:plotlykt-server:0.5.0-dev-1")
|
implementation("space.kscience:plotlykt-server:0.5.3-dev-1")
|
||||||
implementation("com.github.Ricky12Awesome:json-schema-serialization:0.6.6")
|
// implementation("com.github.Ricky12Awesome:json-schema-serialization:0.6.6")
|
||||||
implementation("ch.qos.logback:logback-classic:1.2.3")
|
implementation("ch.qos.logback:logback-classic:1.2.11")
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach {
|
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach {
|
||||||
kotlinOptions {
|
kotlinOptions {
|
||||||
jvmTarget = "11"
|
jvmTarget = "11"
|
||||||
freeCompilerArgs = freeCompilerArgs + "-Xjvm-default=all"
|
freeCompilerArgs = freeCompilerArgs + listOf("-Xjvm-default=all", "-Xopt-in=kotlin.RequiresOptIn")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
javafx {
|
javafx {
|
||||||
version = "14"
|
version = "17"
|
||||||
modules("javafx.controls")
|
modules("javafx.controls")
|
||||||
}
|
}
|
||||||
|
|
||||||
application {
|
application {
|
||||||
mainClass.set("ru.mipt.npm.controls.demo.DemoControllerViewKt")
|
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 io.ktor.server.engine.ApplicationEngine
|
||||||
import javafx.scene.Parent
|
import javafx.scene.Parent
|
||||||
@ -8,21 +8,22 @@ import javafx.stage.Stage
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.eclipse.milo.opcua.sdk.server.OpcUaServer
|
import org.eclipse.milo.opcua.sdk.server.OpcUaServer
|
||||||
import org.eclipse.milo.opcua.stack.core.types.builtin.LocalizedText
|
import org.eclipse.milo.opcua.stack.core.types.builtin.LocalizedText
|
||||||
import ru.mipt.npm.controls.api.DeviceMessage
|
import space.kscience.controls.client.connectToMagix
|
||||||
import ru.mipt.npm.controls.client.connectToMagix
|
import space.kscience.controls.demo.DemoDevice.Companion.cosScale
|
||||||
import ru.mipt.npm.controls.controllers.DeviceManager
|
import space.kscience.controls.demo.DemoDevice.Companion.sinScale
|
||||||
import ru.mipt.npm.controls.controllers.install
|
import space.kscience.controls.demo.DemoDevice.Companion.timeScale
|
||||||
import ru.mipt.npm.controls.demo.DemoDevice.Companion.cosScale
|
import space.kscience.controls.manager.DeviceManager
|
||||||
import ru.mipt.npm.controls.demo.DemoDevice.Companion.sinScale
|
import space.kscience.controls.manager.install
|
||||||
import ru.mipt.npm.controls.demo.DemoDevice.Companion.timeScale
|
import space.kscience.controls.opcua.server.OpcUaServer
|
||||||
import ru.mipt.npm.controls.opcua.server.OpcUaServer
|
import space.kscience.controls.opcua.server.endpoint
|
||||||
import ru.mipt.npm.controls.opcua.server.endpoint
|
import space.kscience.controls.opcua.server.serveDevices
|
||||||
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.dataforge.context.*
|
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 tornadofx.*
|
||||||
import java.awt.Desktop
|
import java.awt.Desktop
|
||||||
import java.net.URI
|
import java.net.URI
|
||||||
@ -33,7 +34,7 @@ class DemoController : Controller(), ContextAware {
|
|||||||
var magixServer: ApplicationEngine? = null
|
var magixServer: ApplicationEngine? = null
|
||||||
var visualizer: ApplicationEngine? = null
|
var visualizer: ApplicationEngine? = null
|
||||||
var opcUaServer: OpcUaServer = OpcUaServer {
|
var opcUaServer: OpcUaServer = OpcUaServer {
|
||||||
setApplicationName(LocalizedText.english("ru.mipt.npm.controls.opcua"))
|
setApplicationName(LocalizedText.english("space.kscience.controls.opcua"))
|
||||||
endpoint {
|
endpoint {
|
||||||
setBindPort(9999)
|
setBindPort(9999)
|
||||||
//use default endpoint
|
//use default endpoint
|
||||||
@ -44,19 +45,24 @@ class DemoController : Controller(), ContextAware {
|
|||||||
plugin(DeviceManager)
|
plugin(DeviceManager)
|
||||||
}
|
}
|
||||||
|
|
||||||
private val deviceManager = context.fetch(DeviceManager)
|
private val deviceManager = context.request(DeviceManager)
|
||||||
|
|
||||||
fun init() {
|
fun init() {
|
||||||
context.launch {
|
context.launch {
|
||||||
device = deviceManager.install("demo", DemoDevice)
|
device = deviceManager.install("demo", DemoDevice)
|
||||||
//starting magix event loop
|
//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
|
//Launch device client and connect it to the server
|
||||||
val deviceEndpoint = MagixEndpoint.rSocketWithTcp("localhost", DeviceMessage.serializer())
|
val deviceEndpoint = MagixEndpoint.rSocketWithTcp("localhost")
|
||||||
deviceManager.connectToMagix(deviceEndpoint)
|
deviceManager.connectToMagix(deviceEndpoint)
|
||||||
val visualEndpoint = MagixEndpoint.rSocketWithWebSockets("localhost", DeviceMessage.serializer())
|
//connect visualization to a magix endpoint
|
||||||
|
val visualEndpoint = MagixEndpoint.rSocketWithWebSockets("localhost")
|
||||||
visualizer = visualEndpoint.startDemoDeviceServer()
|
visualizer = visualEndpoint.startDemoDeviceServer()
|
||||||
|
|
||||||
|
//serve devices as OPC-UA namespace
|
||||||
opcUaServer.startup()
|
opcUaServer.startup()
|
||||||
opcUaServer.serveDevices(deviceManager)
|
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.server.application.install
|
||||||
import io.ktor.features.CORS
|
|
||||||
import io.ktor.server.cio.CIO
|
import io.ktor.server.cio.CIO
|
||||||
import io.ktor.server.engine.ApplicationEngine
|
import io.ktor.server.engine.ApplicationEngine
|
||||||
import io.ktor.server.engine.embeddedServer
|
import io.ktor.server.engine.embeddedServer
|
||||||
import io.ktor.websocket.WebSockets
|
import io.ktor.server.plugins.cors.routing.CORS
|
||||||
import io.rsocket.kotlin.transport.ktor.server.RSocketSupport
|
import io.ktor.server.websocket.WebSockets
|
||||||
|
import io.rsocket.kotlin.ktor.server.RSocketSupport
|
||||||
import kotlinx.coroutines.flow.*
|
import kotlinx.coroutines.flow.*
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.html.div
|
import kotlinx.html.div
|
||||||
import kotlinx.html.link
|
import kotlinx.html.link
|
||||||
import ru.mipt.npm.controls.api.DeviceMessage
|
import space.kscience.controls.api.PropertyChangedMessage
|
||||||
import ru.mipt.npm.controls.api.PropertyChangedMessage
|
import space.kscience.controls.client.controlsMagixFormat
|
||||||
import ru.mipt.npm.magix.api.MagixEndpoint
|
|
||||||
import space.kscience.dataforge.meta.Meta
|
import space.kscience.dataforge.meta.Meta
|
||||||
import space.kscience.dataforge.meta.double
|
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.layout
|
||||||
import space.kscience.plotly.models.Trace
|
import space.kscience.plotly.models.Trace
|
||||||
import space.kscience.plotly.plot
|
import space.kscience.plotly.plot
|
||||||
@ -54,8 +55,8 @@ suspend fun Trace.updateXYFrom(flow: Flow<Iterable<Pair<Double, Double>>>) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
suspend fun MagixEndpoint<DeviceMessage>.startDemoDeviceServer(): ApplicationEngine =
|
@Suppress("ExtractKtorModule")
|
||||||
embeddedServer(CIO, 9090) {
|
suspend fun MagixEndpoint.startDemoDeviceServer(): ApplicationEngine = embeddedServer(CIO, 9091) {
|
||||||
install(WebSockets)
|
install(WebSockets)
|
||||||
install(RSocketSupport)
|
install(RSocketSupport)
|
||||||
|
|
||||||
@ -67,8 +68,8 @@ suspend fun MagixEndpoint<DeviceMessage>.startDemoDeviceServer(): ApplicationEng
|
|||||||
val cosFlow = MutableSharedFlow<Meta?>()// = device.cos.flow()
|
val cosFlow = MutableSharedFlow<Meta?>()// = device.cos.flow()
|
||||||
|
|
||||||
launch {
|
launch {
|
||||||
subscribe().collect { magix ->
|
subscribe(controlsMagixFormat).collect { (_, payload) ->
|
||||||
(magix.payload as? PropertyChangedMessage)?.let { message ->
|
(payload as? PropertyChangedMessage)?.let { message ->
|
||||||
when (message.property) {
|
when (message.property) {
|
||||||
"sin" -> sinFlow.emit(message.value)
|
"sin" -> sinFlow.emit(message.value)
|
||||||
"cos" -> cosFlow.emit(message.value)
|
"cos" -> cosFlow.emit(message.value)
|
||||||
@ -77,10 +78,10 @@ suspend fun MagixEndpoint<DeviceMessage>.startDemoDeviceServer(): ApplicationEng
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
plotlyModule().apply {
|
plotlyModule{
|
||||||
updateMode = PlotlyUpdateMode.PUSH
|
updateMode = PlotlyUpdateMode.PUSH
|
||||||
updateInterval = 50
|
updateInterval = 50
|
||||||
}.page { container ->
|
page { container ->
|
||||||
val sinCosFlow = sinFlow.zip(cosFlow) { sin, cos ->
|
val sinCosFlow = sinFlow.zip(cosFlow) { sin, cos ->
|
||||||
sin.double!! to cos.double!!
|
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 {
|
plugins {
|
||||||
id("ru.mipt.npm.gradle.jvm")
|
id("space.kscience.gradle.jvm")
|
||||||
application
|
application
|
||||||
}
|
}
|
||||||
|
|
@ -7,33 +7,32 @@ import kotlinx.coroutines.flow.onEach
|
|||||||
import kotlinx.coroutines.isActive
|
import kotlinx.coroutines.isActive
|
||||||
import kotlinx.serialization.json.*
|
import kotlinx.serialization.json.*
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import ru.mipt.npm.magix.api.MagixEndpoint
|
import space.kscience.magix.api.MagixEndpoint
|
||||||
import ru.mipt.npm.magix.api.MagixMessage
|
import space.kscience.magix.api.MagixMessage
|
||||||
import ru.mipt.npm.magix.server.startMagixServer
|
import space.kscience.magix.server.RSocketMagixFlowPlugin
|
||||||
import ru.mipt.npm.magix.zmq.ZmqMagixEndpoint
|
import space.kscience.magix.server.startMagixServer
|
||||||
|
import space.kscince.magix.zmq.ZmqMagixEndpoint
|
||||||
|
import space.kscince.magix.zmq.ZmqMagixFlowPlugin
|
||||||
import java.awt.Desktop
|
import java.awt.Desktop
|
||||||
import java.net.URI
|
import java.net.URI
|
||||||
|
|
||||||
|
|
||||||
suspend fun MagixEndpoint<JsonObject>.sendJson(
|
suspend fun MagixEndpoint.sendJson(
|
||||||
origin: String,
|
origin: String,
|
||||||
format: String = "json",
|
format: String = "json",
|
||||||
target: String? = null,
|
target: String? = null,
|
||||||
id: String? = null,
|
id: String? = null,
|
||||||
parentId: String? = null,
|
parentId: String? = null,
|
||||||
user: JsonElement? = null,
|
user: JsonElement? = null,
|
||||||
builder: JsonObjectBuilder.() -> Unit
|
builder: JsonObjectBuilder.() -> Unit,
|
||||||
): Unit = broadcast(MagixMessage(format, origin, buildJsonObject(builder), target, id, parentId, user))
|
): Unit = broadcast(MagixMessage(format, buildJsonObject(builder), origin, target, id, parentId, user))
|
||||||
|
|
||||||
internal const val numberOfMessages = 100
|
internal const val numberOfMessages = 100
|
||||||
|
|
||||||
suspend fun main(): Unit = coroutineScope {
|
suspend fun main(): Unit = coroutineScope {
|
||||||
val logger = LoggerFactory.getLogger("magix-demo")
|
val logger = LoggerFactory.getLogger("magix-demo")
|
||||||
logger.info("Starting magix server")
|
logger.info("Starting magix server")
|
||||||
val server = startMagixServer(
|
val server = startMagixServer(RSocketMagixFlowPlugin(), ZmqMagixFlowPlugin(), buffer = 10)
|
||||||
buffer = 10,
|
|
||||||
enableRawRSocket = false //Disable rsocket to avoid kotlin 1.5 compatibility issue
|
|
||||||
)
|
|
||||||
|
|
||||||
server.apply {
|
server.apply {
|
||||||
val host = "localhost"//environment.connectors.first().host
|
val host = "localhost"//environment.connectors.first().host
|
||||||
@ -44,11 +43,11 @@ suspend fun main(): Unit = coroutineScope {
|
|||||||
|
|
||||||
logger.info("Starting client")
|
logger.info("Starting client")
|
||||||
//Create zmq magix endpoint and wait for to finish
|
//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")
|
logger.info("Starting subscription")
|
||||||
client.subscribe().onEach {
|
client.subscribe().onEach {
|
||||||
println(it.payload)
|
println(it.payload)
|
||||||
if (it.payload["index"]?.jsonPrimitive?.int == numberOfMessages) {
|
if (it.payload.jsonObject["index"]?.jsonPrimitive?.int == numberOfMessages) {
|
||||||
logger.info("Index $numberOfMessages reached. Terminating")
|
logger.info("Index $numberOfMessages reached. Terminating")
|
||||||
cancel()
|
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 {
|
plugins {
|
||||||
id("ru.mipt.npm.gradle.jvm")
|
id("space.kscience.gradle.jvm")
|
||||||
`maven-publish`
|
|
||||||
application
|
application
|
||||||
|
id("org.openjfx.javafxplugin")
|
||||||
}
|
}
|
||||||
|
|
||||||
//TODO to be moved to a separate project
|
//TODO to be moved to a separate project
|
||||||
|
|
||||||
|
javafx {
|
||||||
|
version = "17"
|
||||||
|
modules = listOf("javafx.controls")
|
||||||
|
}
|
||||||
|
|
||||||
application{
|
application{
|
||||||
mainClass.set("ru.mipt.npm.devices.pimotionmaster.PiMotionMasterAppKt")
|
mainClass.set("ru.mipt.npm.devices.pimotionmaster.PiMotionMasterAppKt")
|
||||||
}
|
}
|
||||||
@ -14,14 +19,11 @@ kotlin{
|
|||||||
explicitApi = null
|
explicitApi = null
|
||||||
}
|
}
|
||||||
|
|
||||||
kscience{
|
|
||||||
useFx(ru.mipt.npm.gradle.FXModule.CONTROLS, configuration = ru.mipt.npm.gradle.DependencyConfiguration.IMPLEMENTATION)
|
|
||||||
}
|
|
||||||
|
|
||||||
val ktorVersion: String by rootProject.extra
|
val ktorVersion: String by rootProject.extra
|
||||||
|
val dataforgeVersion: String by extra
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(project(":controls-tcp"))
|
implementation(project(":controls-ktor-tcp"))
|
||||||
implementation(project(":controls-magix-client"))
|
implementation(project(":controls-magix-client"))
|
||||||
implementation("no.tornado:tornadofx:1.7.20")
|
implementation("no.tornado:tornadofx:1.7.20")
|
||||||
}
|
}
|
@ -11,9 +11,12 @@ import javafx.scene.layout.VBox
|
|||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import ru.mipt.npm.controls.controllers.DeviceManager
|
import ru.mipt.npm.devices.pimotionmaster.PiMotionMasterDevice.Axis.Companion.maxPosition
|
||||||
import ru.mipt.npm.controls.controllers.installing
|
import ru.mipt.npm.devices.pimotionmaster.PiMotionMasterDevice.Axis.Companion.minPosition
|
||||||
import space.kscience.dataforge.context.Global
|
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 space.kscience.dataforge.context.fetch
|
||||||
import tornadofx.*
|
import tornadofx.*
|
||||||
|
|
||||||
@ -21,7 +24,7 @@ class PiMotionMasterApp : App(PiMotionMasterView::class)
|
|||||||
|
|
||||||
class PiMotionMasterController : Controller() {
|
class PiMotionMasterController : Controller() {
|
||||||
//initialize context
|
//initialize context
|
||||||
val context = Global.buildContext("piMotionMaster"){
|
val context = Context("piMotionMaster"){
|
||||||
plugin(DeviceManager)
|
plugin(DeviceManager)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -40,10 +43,11 @@ fun VBox.piMotionMasterAxis(
|
|||||||
alignment = Pos.CENTER
|
alignment = Pos.CENTER
|
||||||
label(axisName)
|
label(axisName)
|
||||||
coroutineScope.launch {
|
coroutineScope.launch {
|
||||||
val min = axis.minPosition.readTyped(true)
|
with(axis) {
|
||||||
val max = axis.maxPosition.readTyped(true)
|
val min: Double = minPosition.read()
|
||||||
val positionProperty = axis.position.fxProperty(axis)
|
val max: Double = maxPosition.read()
|
||||||
val startPosition = axis.position.readTyped(true)
|
val positionProperty = fxProperty(position)
|
||||||
|
val startPosition = position.read()
|
||||||
runLater {
|
runLater {
|
||||||
vbox {
|
vbox {
|
||||||
hgrow = Priority.ALWAYS
|
hgrow = Priority.ALWAYS
|
||||||
@ -67,6 +71,7 @@ fun VBox.piMotionMasterAxis(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun Parent.axisPane(axes: Map<String, PiMotionMasterDevice.Axis>, coroutineScope: CoroutineScope) {
|
fun Parent.axisPane(axes: Map<String, PiMotionMasterDevice.Axis>, coroutineScope: CoroutineScope) {
|
||||||
vbox {
|
vbox {
|
||||||
@ -82,7 +87,7 @@ class PiMotionMasterView : View() {
|
|||||||
private val controller: PiMotionMasterController by inject()
|
private val controller: PiMotionMasterController by inject()
|
||||||
val device = controller.motionMaster
|
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 debugServerJobProperty = SimpleObjectProperty<Job>()
|
||||||
private val debugServerStarted = debugServerJobProperty.booleanBinding { it != null }
|
private val debugServerStarted = debugServerJobProperty.booleanBinding { it != null }
|
||||||
//private val axisList = FXCollections.observableArrayList<Map.Entry<String, PiMotionMasterDevice.Axis>>()
|
//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.flow.*
|
||||||
import kotlinx.coroutines.sync.Mutex
|
import kotlinx.coroutines.sync.Mutex
|
||||||
import kotlinx.coroutines.sync.withLock
|
import kotlinx.coroutines.sync.withLock
|
||||||
import ru.mipt.npm.controls.api.Socket
|
import space.kscience.controls.api.Socket
|
||||||
import ru.mipt.npm.controls.ports.AbstractPort
|
import space.kscience.controls.ports.AbstractPort
|
||||||
import ru.mipt.npm.controls.ports.withDelimiter
|
import space.kscience.controls.ports.withDelimiter
|
||||||
import space.kscience.dataforge.context.*
|
import space.kscience.dataforge.context.*
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
import kotlin.time.Duration
|
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.util.moveToByteArray
|
||||||
import io.ktor.utils.io.writeAvailable
|
import io.ktor.utils.io.writeAvailable
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.coroutines.flow.collect
|
|
||||||
import space.kscience.dataforge.context.Context
|
import space.kscience.dataforge.context.Context
|
||||||
import space.kscience.dataforge.context.Global
|
import space.kscience.dataforge.context.Global
|
||||||
import java.net.InetSocketAddress
|
|
||||||
|
|
||||||
val exceptionHandler = CoroutineExceptionHandler { _, throwable ->
|
val exceptionHandler = CoroutineExceptionHandler { _, throwable ->
|
||||||
throwable.printStackTrace()
|
throwable.printStackTrace()
|
||||||
@ -20,7 +18,7 @@ val exceptionHandler = CoroutineExceptionHandler { _, throwable ->
|
|||||||
@OptIn(InternalAPI::class)
|
@OptIn(InternalAPI::class)
|
||||||
fun Context.launchPiDebugServer(port: Int, axes: List<String>): Job = launch(exceptionHandler) {
|
fun Context.launchPiDebugServer(port: Int, axes: List<String>): Job = launch(exceptionHandler) {
|
||||||
val virtualDevice = PiMotionMasterVirtualDevice(this@launchPiDebugServer, axes)
|
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}")
|
println("Started virtual port server at ${server.localAddress}")
|
||||||
|
|
||||||
while (isActive) {
|
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
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
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
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
|
@ -1,9 +1,12 @@
|
|||||||
plugins {
|
plugins {
|
||||||
id("ru.mipt.npm.gradle.mpp")
|
id("space.kscience.gradle.mpp")
|
||||||
`maven-publish`
|
`maven-publish`
|
||||||
}
|
}
|
||||||
|
|
||||||
kscience {
|
kscience {
|
||||||
|
jvm()
|
||||||
|
js()
|
||||||
|
native()
|
||||||
useCoroutines()
|
useCoroutines()
|
||||||
useSerialization{
|
useSerialization{
|
||||||
json()
|
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