Compare commits

...

2 Commits

10 changed files with 130 additions and 58 deletions

View File

@ -6,14 +6,14 @@ plugins {
} }
val dataforgeVersion: String by extra("0.7.1") val dataforgeVersion: String by extra("0.7.1")
val visionforgeVersion by extra("0.3.0-RC") val visionforgeVersion by extra("0.3.1")
val ktorVersion: String by extra(space.kscience.gradle.KScienceVersions.ktorVersion) val ktorVersion: String by extra(space.kscience.gradle.KScienceVersions.ktorVersion)
val rsocketVersion by extra("0.15.4") val rsocketVersion by extra("0.15.4")
val xodusVersion by extra("2.0.1") val xodusVersion by extra("2.0.1")
allprojects { allprojects {
group = "space.kscience" group = "space.kscience"
version = "0.3.0-dev-4" version = "0.3.0-dev-6"
repositories{ repositories{
maven("https://maven.pkg.jetbrains.space/spc/p/sci/dev") maven("https://maven.pkg.jetbrains.space/spc/p/sci/dev")
} }

View File

@ -2,7 +2,7 @@ package space.kscience.controls.constructor
import space.kscience.controls.api.Device import space.kscience.controls.api.Device
import space.kscience.controls.api.PropertyDescriptor import space.kscience.controls.api.PropertyDescriptor
import space.kscience.controls.manager.DeviceManager import space.kscience.dataforge.context.Context
import space.kscience.dataforge.context.Factory import space.kscience.dataforge.context.Factory
import space.kscience.dataforge.meta.Meta import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.transformations.MetaConverter import space.kscience.dataforge.meta.transformations.MetaConverter
@ -18,9 +18,9 @@ import kotlin.time.Duration
* A base for strongly typed device constructor blocks. Has additional delegates for type-safe devices * A base for strongly typed device constructor blocks. Has additional delegates for type-safe devices
*/ */
public abstract class DeviceConstructor( public abstract class DeviceConstructor(
deviceManager: DeviceManager, context: Context,
meta: Meta, meta: Meta,
) : DeviceGroup(deviceManager, meta) { ) : DeviceGroup(context, meta) {
/** /**
* Register a device, provided by a given [factory] and * Register a device, provided by a given [factory] and
@ -57,8 +57,8 @@ public abstract class DeviceConstructor(
*/ */
public fun <T : Any> property( public fun <T : Any> property(
state: DeviceState<T>, state: DeviceState<T>,
nameOverride: String? = null,
descriptorBuilder: PropertyDescriptor.() -> Unit = {}, descriptorBuilder: PropertyDescriptor.() -> Unit = {},
nameOverride: String? = null,
): PropertyDelegateProvider<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, T>> = ): PropertyDelegateProvider<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, T>> =
PropertyDelegateProvider { _: DeviceConstructor, property -> PropertyDelegateProvider { _: DeviceConstructor, property ->
val name = nameOverride ?: property.name val name = nameOverride ?: property.name
@ -77,11 +77,12 @@ public abstract class DeviceConstructor(
reader: suspend () -> T, reader: suspend () -> T,
readInterval: Duration, readInterval: Duration,
initialState: T, initialState: T,
nameOverride: String? = null,
descriptorBuilder: PropertyDescriptor.() -> Unit = {}, descriptorBuilder: PropertyDescriptor.() -> Unit = {},
nameOverride: String? = null,
): PropertyDelegateProvider<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, T>> = property( ): PropertyDelegateProvider<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, T>> = property(
DeviceState.external(this, metaConverter, readInterval, initialState, reader), DeviceState.external(this, metaConverter, readInterval, initialState, reader),
nameOverride, descriptorBuilder descriptorBuilder,
nameOverride,
) )
@ -90,8 +91,8 @@ public abstract class DeviceConstructor(
*/ */
public fun <T : Any> mutableProperty( public fun <T : Any> mutableProperty(
state: MutableDeviceState<T>, state: MutableDeviceState<T>,
nameOverride: String? = null,
descriptorBuilder: PropertyDescriptor.() -> Unit = {}, descriptorBuilder: PropertyDescriptor.() -> Unit = {},
nameOverride: String? = null,
): PropertyDelegateProvider<DeviceConstructor, ReadWriteProperty<DeviceConstructor, T>> = ): PropertyDelegateProvider<DeviceConstructor, ReadWriteProperty<DeviceConstructor, T>> =
PropertyDelegateProvider { _: DeviceConstructor, property -> PropertyDelegateProvider { _: DeviceConstructor, property ->
val name = nameOverride ?: property.name val name = nameOverride ?: property.name
@ -116,11 +117,26 @@ public abstract class DeviceConstructor(
writer: suspend (T) -> Unit, writer: suspend (T) -> Unit,
readInterval: Duration, readInterval: Duration,
initialState: T, initialState: T,
nameOverride: String? = null,
descriptorBuilder: PropertyDescriptor.() -> Unit = {}, descriptorBuilder: PropertyDescriptor.() -> Unit = {},
nameOverride: String? = null,
): PropertyDelegateProvider<DeviceConstructor, ReadWriteProperty<DeviceConstructor, T>> = mutableProperty( ): PropertyDelegateProvider<DeviceConstructor, ReadWriteProperty<DeviceConstructor, T>> = mutableProperty(
DeviceState.external(this, metaConverter, readInterval, initialState, reader, writer), DeviceState.external(this, metaConverter, readInterval, initialState, reader, writer),
descriptorBuilder,
nameOverride,
)
/**
* Create and register a virtual property with optional [callback]
*/
public fun <T : Any> state(
metaConverter: MetaConverter<T>,
initialState: T,
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
nameOverride: String? = null,
callback: (T) -> Unit = {},
): PropertyDelegateProvider<DeviceConstructor, ReadWriteProperty<DeviceConstructor, T>> = mutableProperty(
DeviceState.virtual(metaConverter, initialState, callback),
descriptorBuilder,
nameOverride, nameOverride,
descriptorBuilder
) )
} }

View File

@ -27,7 +27,7 @@ import kotlin.coroutines.CoroutineContext
* A mutable group of devices and properties to be used for lightweight design and simulations. * A mutable group of devices and properties to be used for lightweight design and simulations.
*/ */
public open class DeviceGroup( public open class DeviceGroup(
public val deviceManager: DeviceManager, final override val context: Context,
override val meta: Meta, override val meta: Meta,
) : DeviceHub, CachingDevice { ) : DeviceHub, CachingDevice {
@ -42,17 +42,15 @@ public open class DeviceGroup(
) )
override final val context: Context get() = deviceManager.context
private val sharedMessageFlow = MutableSharedFlow<DeviceMessage>() private val sharedMessageFlow = MutableSharedFlow<DeviceMessage>()
override val messageFlow: Flow<DeviceMessage> override val messageFlow: Flow<DeviceMessage>
get() = sharedMessageFlow get() = sharedMessageFlow
@OptIn(ExperimentalCoroutinesApi::class)
override val coroutineContext: CoroutineContext = context.newCoroutineContext( override val coroutineContext: CoroutineContext = context.newCoroutineContext(
SupervisorJob(context.coroutineContext[Job]) + SupervisorJob(context.coroutineContext[Job]) +
CoroutineName("Device $this") + CoroutineName("Device $id") +
CoroutineExceptionHandler { _, throwable -> CoroutineExceptionHandler { _, throwable ->
context.launch { context.launch {
sharedMessageFlow.emit( sharedMessageFlow.emit(
@ -78,7 +76,7 @@ public open class DeviceGroup(
public fun <D : Device> install(token: NameToken, device: D): D { public fun <D : Device> install(token: NameToken, device: D): D {
require(_devices[token] == null) { "A child device with name $token already exists" } require(_devices[token] == null) { "A child device with name $token already exists" }
//start the child device if needed //start the child device if needed
if(lifecycleState == STARTED || lifecycleState == STARTING) launch { device.start() } if (lifecycleState == STARTED || lifecycleState == STARTING) launch { device.start() }
_devices[token] = device _devices[token] = device
return device return device
} }
@ -175,7 +173,7 @@ public fun DeviceManager.registerDeviceGroup(
meta: Meta = Meta.EMPTY, meta: Meta = Meta.EMPTY,
block: DeviceGroup.() -> Unit, block: DeviceGroup.() -> Unit,
): DeviceGroup { ): DeviceGroup {
val group = DeviceGroup(this, meta).apply(block) val group = DeviceGroup(context, meta).apply(block)
install(name, group) install(name, group)
return group return group
} }
@ -194,7 +192,7 @@ private fun DeviceGroup.getOrCreateGroup(name: Name): DeviceGroup {
when (val d = devices[token]) { when (val d = devices[token]) {
null -> install( null -> install(
token, token,
DeviceGroup(deviceManager, meta[token] ?: Meta.EMPTY) DeviceGroup(context, meta[token] ?: Meta.EMPTY)
) )
else -> (d as? DeviceGroup) ?: error("Device $name is not a DeviceGroup") else -> (d as? DeviceGroup) ?: error("Device $name is not a DeviceGroup")
@ -219,6 +217,9 @@ public fun <D : Device> DeviceGroup.install(name: Name, device: D): D {
public fun <D : Device> DeviceGroup.install(name: String, device: D): D = public fun <D : Device> DeviceGroup.install(name: String, device: D): D =
install(name.parseAsName(), device) install(name.parseAsName(), device)
public fun <D : Device> DeviceGroup.install(device: D): D =
install(device.id, device)
public fun <D : Device> Context.install(name: String, device: D): D = request(DeviceManager).install(name, device) public fun <D : Device> Context.install(name: String, device: D): D = request(DeviceManager).install(name, device)
/** /**
@ -234,7 +235,7 @@ public fun <D : Device> DeviceGroup.install(
deviceMeta: Meta? = null, deviceMeta: Meta? = null,
metaLocation: Name = name, metaLocation: Name = name,
): D { ): D {
val newDevice = factory.build(deviceManager.context, Laminate(deviceMeta, meta[metaLocation])) val newDevice = factory.build(context, Laminate(deviceMeta, meta[metaLocation]))
install(name, newDevice) install(name, newDevice)
return newDevice return newDevice
} }
@ -284,15 +285,6 @@ public fun <T : Any> DeviceGroup.registerMutableProperty(
} }
/**
* Create a virtual [MutableDeviceState], but do not register it to a device
*/
@Suppress("UnusedReceiverParameter")
public fun <T : Any> DeviceGroup.state(
converter: MetaConverter<T>,
initialValue: T,
): MutableDeviceState<T> = VirtualDeviceState<T>(converter, initialValue)
/** /**
* Create a new virtual mutable state and a property based on it. * Create a new virtual mutable state and a property based on it.
* @return the mutable state used in property * @return the mutable state used in property
@ -302,8 +294,9 @@ public fun <T : Any> DeviceGroup.registerVirtualProperty(
initialValue: T, initialValue: T,
converter: MetaConverter<T>, converter: MetaConverter<T>,
descriptorBuilder: PropertyDescriptor.() -> Unit = {}, descriptorBuilder: PropertyDescriptor.() -> Unit = {},
callback: (T) -> Unit = {},
): MutableDeviceState<T> { ): MutableDeviceState<T> {
val state = state(converter, initialValue) val state = DeviceState.virtual<T>(converter, initialValue, callback)
registerMutableProperty(name, state, descriptorBuilder) registerMutableProperty(name, state, descriptorBuilder)
return state return state
} }

View File

@ -45,17 +45,49 @@ public var <T : Any> MutableDeviceState<T>.valueAsMeta: Meta
/** /**
* A [MutableDeviceState] that does not correspond to a physical state * A [MutableDeviceState] that does not correspond to a physical state
*
* @param callback a synchronous callback that could be used without a scope
*/ */
public class VirtualDeviceState<T>( private class VirtualDeviceState<T>(
override val converter: MetaConverter<T>, override val converter: MetaConverter<T>,
initialValue: T, initialValue: T,
private val callback: (T) -> Unit = {},
) : MutableDeviceState<T> { ) : MutableDeviceState<T> {
private val flow = MutableStateFlow(initialValue) private val flow = MutableStateFlow(initialValue)
override val valueFlow: Flow<T> get() = flow override val valueFlow: Flow<T> get() = flow
override var value: T by flow::value override var value: T
get() = flow.value
set(value) {
flow.value = value
callback(value)
}
} }
/**
* A [MutableDeviceState] that does not correspond to a physical state
*
* @param callback a synchronous callback that could be used without a scope
*/
public fun <T> DeviceState.Companion.virtual(
converter: MetaConverter<T>,
initialValue: T,
callback: (T) -> Unit = {},
): MutableDeviceState<T> = VirtualDeviceState(converter, initialValue, callback)
private class StateFlowAsState<T>(
override val converter: MetaConverter<T>,
val flow: MutableStateFlow<T>,
) : MutableDeviceState<T> {
override var value: T by flow::value
override val valueFlow: Flow<T> get() = flow
}
public fun <T> MutableStateFlow<T>.asDeviceState(converter: MetaConverter<T>): DeviceState<T> =
StateFlowAsState(converter, this)
private open class BoundDeviceState<T>( private open class BoundDeviceState<T>(
override val converter: MetaConverter<T>, override val converter: MetaConverter<T>,
val device: Device, val device: Device,

View File

@ -10,6 +10,8 @@ import space.kscience.dataforge.context.ContextAware
import space.kscience.dataforge.context.info import space.kscience.dataforge.context.info
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.dataforge.meta.get
import space.kscience.dataforge.meta.string
import space.kscience.dataforge.misc.DFExperimental import space.kscience.dataforge.misc.DFExperimental
import space.kscience.dataforge.misc.DfType import space.kscience.dataforge.misc.DfType
import space.kscience.dataforge.names.parseAsName import space.kscience.dataforge.names.parseAsName
@ -111,6 +113,11 @@ public interface Device : ContextAware, CoroutineScope {
} }
} }
/**
* Inner id of a device. Not necessary corresponds to the name in the parent container
*/
public val Device.id: String get() = meta["id"].string?: "device[${hashCode().toString(16)}]"
/** /**
* Device that caches properties values * Device that caches properties values
*/ */

View File

@ -4,6 +4,7 @@ import kotlinx.coroutines.launch
import space.kscience.controls.api.Device import space.kscience.controls.api.Device
import space.kscience.controls.api.DeviceHub import space.kscience.controls.api.DeviceHub
import space.kscience.controls.api.getOrNull import space.kscience.controls.api.getOrNull
import space.kscience.controls.api.id
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
@ -45,6 +46,8 @@ public fun <D : Device> DeviceManager.install(name: String, device: D): D {
return device return device
} }
public fun <D : Device> DeviceManager.install(device: D): D = install(device.id, device)
/** /**
* Register and start a device built by [factory] with current [Context] and [meta]. * Register and start a device built by [factory] with current [Context] and [meta].

View File

@ -9,6 +9,8 @@ import kotlinx.io.readByteArray
/** /**
* Transform byte fragments into complete phrases using given delimiter. Not thread safe. * Transform byte fragments into complete phrases using given delimiter. Not thread safe.
*
* TODO add type wrapper for phrases
*/ */
public fun Flow<ByteArray>.withDelimiter(delimiter: ByteArray): 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" }

View File

@ -81,7 +81,7 @@ public suspend fun <T, D : Device> D.read(propertySpec: DevicePropertySpec<D, T>
public suspend fun <T, D : DeviceBase<D>> D.readOrNull(propertySpec: DevicePropertySpec<D, T>): T? = public suspend fun <T, D : DeviceBase<D>> D.readOrNull(propertySpec: DevicePropertySpec<D, T>): T? =
readPropertyOrNull(propertySpec.name)?.let(propertySpec.converter::metaToObject) readPropertyOrNull(propertySpec.name)?.let(propertySpec.converter::metaToObject)
public suspend fun <T, D : Device> D.request(propertySpec: DevicePropertySpec<D, T>): T? = public suspend fun <T, D : Device> D.request(propertySpec: DevicePropertySpec<D, T>): T =
propertySpec.converter.metaToObject(requestProperty(propertySpec.name)) propertySpec.converter.metaToObject(requestProperty(propertySpec.name))
/** /**

View File

@ -18,6 +18,8 @@ import space.kscience.controls.api.propertyMessageFlow
import space.kscience.controls.constructor.DeviceState import space.kscience.controls.constructor.DeviceState
import space.kscience.controls.manager.clock import space.kscience.controls.manager.clock
import space.kscience.controls.misc.ValueWithTime import space.kscience.controls.misc.ValueWithTime
import space.kscience.controls.spec.DevicePropertySpec
import space.kscience.controls.spec.name
import space.kscience.dataforge.context.Context import space.kscience.dataforge.context.Context
import space.kscience.dataforge.meta.* import space.kscience.dataforge.meta.*
import space.kscience.plotly.Plot import space.kscience.plotly.Plot
@ -28,8 +30,8 @@ import space.kscience.plotly.models.Trace
import space.kscience.plotly.models.TraceValues import space.kscience.plotly.models.TraceValues
import space.kscience.plotly.scatter import space.kscience.plotly.scatter
import kotlin.time.Duration import kotlin.time.Duration
import kotlin.time.Duration.Companion.hours import kotlin.time.Duration.Companion.minutes
import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.seconds
private var TraceValues.values: List<Value> private var TraceValues.values: List<Value>
get() = value?.list ?: emptyList() get() = value?.list ?: emptyList()
@ -82,6 +84,11 @@ private class TimeData(private var points: MutableList<ValueWithTime<Value>> = m
} }
} }
private val defaultMaxAge get() = 10.minutes
private val defaultMaxPoints get() = 800
private val defaultMinPoints get() = 400
private val defaultSampling get() = 1.seconds
/** /**
* Add a trace that shows a [Device] property change over time. Show only latest [maxPoints] . * Add a trace that shows a [Device] property change over time. Show only latest [maxPoints] .
* @return a [Job] that handles the listener * @return a [Job] that handles the listener
@ -90,10 +97,10 @@ public fun Plot.plotDeviceProperty(
device: Device, device: Device,
propertyName: String, propertyName: String,
extractValue: Meta.() -> Value = { value ?: Null }, extractValue: Meta.() -> Value = { value ?: Null },
maxAge: Duration = 1.hours, maxAge: Duration = defaultMaxAge,
maxPoints: Int = 800, maxPoints: Int = defaultMaxPoints,
minPoints: Int = 400, minPoints: Int = defaultMinPoints,
sampling: Duration = 10.milliseconds, sampling: Duration = defaultSampling,
coroutineScope: CoroutineScope = device.context, coroutineScope: CoroutineScope = device.context,
configuration: Scatter.() -> Unit = {}, configuration: Scatter.() -> Unit = {},
): Job = scatter(configuration).run { ): Job = scatter(configuration).run {
@ -108,14 +115,27 @@ public fun Plot.plotDeviceProperty(
}.launchIn(coroutineScope) }.launchIn(coroutineScope)
} }
public fun Plot.plotDeviceProperty(
device: Device,
property: DevicePropertySpec<*, Number>,
maxAge: Duration = defaultMaxAge,
maxPoints: Int = defaultMaxPoints,
minPoints: Int = defaultMinPoints,
sampling: Duration = defaultSampling,
coroutineScope: CoroutineScope = device.context,
configuration: Scatter.() -> Unit = {},
): Job = plotDeviceProperty(
device, property.name, { value ?: Null }, maxAge, maxPoints, minPoints, sampling, coroutineScope, configuration
)
private fun <T> Trace.updateFromState( private fun <T> Trace.updateFromState(
context: Context, context: Context,
state: DeviceState<T>, state: DeviceState<T>,
extractValue: T.() -> Value = { state.converter.objectToMeta(this).value ?: space.kscience.dataforge.meta.Null }, extractValue: T.() -> Value,
maxAge: Duration = 1.hours, maxAge: Duration,
maxPoints: Int = 800, maxPoints: Int,
minPoints: Int = 400, minPoints: Int,
sampling: Duration = 10.milliseconds, sampling: Duration,
): Job { ): Job {
val clock = context.clock val clock = context.clock
val data = TimeData() val data = TimeData()
@ -131,10 +151,10 @@ public fun <T> Plot.plotDeviceState(
context: Context, context: Context,
state: DeviceState<T>, state: DeviceState<T>,
extractValue: T.() -> Value = { state.converter.objectToMeta(this).value ?: Null }, extractValue: T.() -> Value = { state.converter.objectToMeta(this).value ?: Null },
maxAge: Duration = 1.hours, maxAge: Duration = defaultMaxAge,
maxPoints: Int = 800, maxPoints: Int = defaultMaxPoints,
minPoints: Int = 400, minPoints: Int = defaultMinPoints,
sampling: Duration = 10.milliseconds, sampling: Duration = defaultSampling,
configuration: Scatter.() -> Unit = {}, configuration: Scatter.() -> Unit = {},
): Job = scatter(configuration).run { ): Job = scatter(configuration).run {
updateFromState(context, state, extractValue, maxAge, maxPoints, minPoints, sampling) updateFromState(context, state, extractValue, maxAge, maxPoints, minPoints, sampling)
@ -144,10 +164,10 @@ public fun <T> Plot.plotDeviceState(
public fun Plot.plotNumberState( public fun Plot.plotNumberState(
context: Context, context: Context,
state: DeviceState<out Number>, state: DeviceState<out Number>,
maxAge: Duration = 1.hours, maxAge: Duration = defaultMaxAge,
maxPoints: Int = 800, maxPoints: Int = defaultMaxPoints,
minPoints: Int = 400, minPoints: Int = defaultMinPoints,
sampling: Duration = 10.milliseconds, sampling: Duration = defaultSampling,
configuration: Scatter.() -> Unit = {}, configuration: Scatter.() -> Unit = {},
): Job = scatter(configuration).run { ): Job = scatter(configuration).run {
updateFromState(context, state, { asValue() }, maxAge, maxPoints, minPoints, sampling) updateFromState(context, state, { asValue() }, maxAge, maxPoints, minPoints, sampling)
@ -157,10 +177,10 @@ public fun Plot.plotNumberState(
public fun Plot.plotBooleanState( public fun Plot.plotBooleanState(
context: Context, context: Context,
state: DeviceState<Boolean>, state: DeviceState<Boolean>,
maxAge: Duration = 1.hours, maxAge: Duration = defaultMaxAge,
maxPoints: Int = 800, maxPoints: Int = defaultMaxPoints,
minPoints: Int = 400, minPoints: Int = defaultMinPoints,
sampling: Duration = 10.milliseconds, sampling: Duration = defaultSampling,
configuration: Bar.() -> Unit = {}, configuration: Bar.() -> Unit = {},
): Job = bar(configuration).run { ): Job = bar(configuration).run {
updateFromState(context, state, { asValue() }, maxAge, maxPoints, minPoints, sampling) updateFromState(context, state, { asValue() }, maxAge, maxPoints, minPoints, sampling)

View File

@ -26,7 +26,6 @@ import space.kscience.controls.vision.plotDeviceProperty
import space.kscience.controls.vision.plotNumberState import space.kscience.controls.vision.plotNumberState
import space.kscience.controls.vision.showDashboard import space.kscience.controls.vision.showDashboard
import space.kscience.dataforge.context.Context import space.kscience.dataforge.context.Context
import space.kscience.dataforge.context.request
import space.kscience.dataforge.meta.Meta import space.kscience.dataforge.meta.Meta
import space.kscience.plotly.models.ScatterMode import space.kscience.plotly.models.ScatterMode
import space.kscience.visionforge.plotly.PlotlyPlugin import space.kscience.visionforge.plotly.PlotlyPlugin
@ -44,7 +43,7 @@ class LinearDrive(
mass: Double, mass: Double,
pidParameters: PidParameters, pidParameters: PidParameters,
meta: Meta = Meta.EMPTY, meta: Meta = Meta.EMPTY,
) : DeviceConstructor(context.request(DeviceManager), meta) { ) : DeviceConstructor(context, meta) {
val drive by device(VirtualDrive.factory(mass, state)) val drive by device(VirtualDrive.factory(mass, state))
val pid by device(PidRegulator(drive, pidParameters)) val pid by device(PidRegulator(drive, pidParameters))