Add DeviceConstructor

This commit is contained in:
Alexander Nozik 2023-11-06 16:46:16 +03:00
parent 0443fdc3c0
commit 825f1a4d04
11 changed files with 297 additions and 67 deletions

View File

@ -0,0 +1,126 @@
package space.kscience.controls.constructor
import space.kscience.controls.api.Device
import space.kscience.controls.api.PropertyDescriptor
import space.kscience.controls.manager.DeviceManager
import space.kscience.dataforge.context.Factory
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.transformations.MetaConverter
import space.kscience.dataforge.names.Name
import space.kscience.dataforge.names.asName
import kotlin.properties.PropertyDelegateProvider
import kotlin.properties.ReadOnlyProperty
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty
import kotlin.time.Duration
/**
* A base for strongly typed device constructor blocks. Has additional delegates for type-safe devices
*/
public abstract class DeviceConstructor(
deviceManager: DeviceManager,
meta: Meta,
) : DeviceGroup(deviceManager, meta) {
/**
* Register a device, provided by a given [factory] and
*/
public fun <D : Device> device(
factory: Factory<D>,
meta: Meta? = null,
nameOverride: Name? = null,
metaLocation: Name? = null,
): PropertyDelegateProvider<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, D>> =
PropertyDelegateProvider { _: DeviceConstructor, property: KProperty<*> ->
val name = nameOverride ?: property.name.asName()
val device = registerDevice(name, factory, meta, metaLocation ?: name)
ReadOnlyProperty { _: DeviceConstructor, _ ->
device
}
}
public fun <D : Device> device(
device: D,
nameOverride: Name? = null,
): PropertyDelegateProvider<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, D>> =
PropertyDelegateProvider { _: DeviceConstructor, property: KProperty<*> ->
val name = nameOverride ?: property.name.asName()
registerDevice(name, device)
ReadOnlyProperty { _: DeviceConstructor, _ ->
device
}
}
/**
* Register a property and provide a direct reader for it
*/
public fun <T : Any> property(
state: DeviceState<T>,
nameOverride: String? = null,
descriptorBuilder: PropertyDescriptor.() -> Unit,
): PropertyDelegateProvider<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, T>> =
PropertyDelegateProvider { _: DeviceConstructor, property ->
val name = nameOverride ?: property.name
val descriptor = PropertyDescriptor(name).apply(descriptorBuilder)
registerProperty(descriptor, state)
ReadOnlyProperty { _: DeviceConstructor, _ ->
state.value
}
}
/**
* Register external state as a property
*/
public fun <T : Any> property(
metaConverter: MetaConverter<T>,
reader: suspend () -> T,
readInterval: Duration,
initialState: T,
nameOverride: String? = null,
descriptorBuilder: PropertyDescriptor.() -> Unit,
): PropertyDelegateProvider<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, T>> = property(
DeviceState.external(this, metaConverter, readInterval, initialState, reader),
nameOverride, descriptorBuilder
)
/**
* Register a mutable property and provide a direct reader for it
*/
public fun <T : Any> mutableProperty(
state: MutableDeviceState<T>,
nameOverride: String? = null,
descriptorBuilder: PropertyDescriptor.() -> Unit,
): PropertyDelegateProvider<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, T>> =
PropertyDelegateProvider { _: DeviceConstructor, property ->
val name = nameOverride ?: property.name
val descriptor = PropertyDescriptor(name).apply(descriptorBuilder)
registerProperty(descriptor, state)
object : ReadWriteProperty<DeviceConstructor, T> {
override fun getValue(thisRef: DeviceConstructor, property: KProperty<*>): T = state.value
override fun setValue(thisRef: DeviceConstructor, property: KProperty<*>, value: T) {
state.value = value
}
}
}
/**
* Register external state as a property
*/
public fun <T : Any> mutableProperty(
metaConverter: MetaConverter<T>,
reader: suspend () -> T,
writer: suspend (T) -> Unit,
readInterval: Duration,
initialState: T,
nameOverride: String? = null,
descriptorBuilder: PropertyDescriptor.() -> Unit,
): PropertyDelegateProvider<DeviceConstructor, ReadOnlyProperty<DeviceConstructor, T>> = mutableProperty(
DeviceState.external(this, metaConverter, readInterval, initialState, reader, writer),
nameOverride,
descriptorBuilder
)
}

View File

@ -4,6 +4,7 @@ import kotlinx.coroutines.*
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
import space.kscience.controls.api.* import space.kscience.controls.api.*
import space.kscience.controls.api.DeviceLifecycleState.*
import space.kscience.controls.manager.DeviceManager import space.kscience.controls.manager.DeviceManager
import space.kscience.controls.manager.install import space.kscience.controls.manager.install
import space.kscience.dataforge.context.Context import space.kscience.dataforge.context.Context
@ -23,7 +24,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 class DeviceGroup( public open class DeviceGroup(
public val deviceManager: DeviceManager, public val deviceManager: DeviceManager,
override val meta: Meta, override val meta: Meta,
) : DeviceHub, CachingDevice { ) : DeviceHub, CachingDevice {
@ -63,15 +64,28 @@ public class DeviceGroup(
override val devices: Map<NameToken, Device> = _devices override val devices: Map<NameToken, Device> = _devices
public fun <D : Device> device(token: NameToken, device: D): D { /**
check(_devices[token] == null) { "A child device with name $token already exists" } * Register and initialize (synchronize child's lifecycle state with group state) a new device in this group
*/
@OptIn(DFExperimental::class)
public fun <D : Device> registerDevice(token: NameToken, device: D): D {
require(_devices[token] == null) { "A child device with name $token already exists" }
//start or stop the child if needed
when (lifecycleState) {
STARTING, STARTED -> launch { device.start() }
STOPPED -> device.stop()
ERROR -> {}
}
_devices[token] = device _devices[token] = device
return device return device
} }
private val properties: MutableMap<Name, Property> = hashMapOf() private val properties: MutableMap<Name, Property> = hashMapOf()
public fun property(descriptor: PropertyDescriptor, state: DeviceState<out Any>) { /**
* Register a new property based on [DeviceState]. Properties could be modified dynamically
*/
public fun registerProperty(descriptor: PropertyDescriptor, state: DeviceState<out Any>) {
val name = descriptor.name.parseAsName() val name = descriptor.name.parseAsName()
require(properties[name] == null) { "Can't add property with name $name. It already exists." } require(properties[name] == null) { "Can't add property with name $name. It already exists." }
properties[name] = Property(state, descriptor) properties[name] = Property(state, descriptor)
@ -112,8 +126,8 @@ public class DeviceGroup(
} }
@DFExperimental @DFExperimental
override var lifecycleState: DeviceLifecycleState = DeviceLifecycleState.STOPPED override var lifecycleState: DeviceLifecycleState = STOPPED
private set(value) { protected set(value) {
if (field != value) { if (field != value) {
launch { launch {
sharedMessageFlow.emit( sharedMessageFlow.emit(
@ -127,12 +141,12 @@ public class DeviceGroup(
@OptIn(DFExperimental::class) @OptIn(DFExperimental::class)
override suspend fun start() { override suspend fun start() {
lifecycleState = DeviceLifecycleState.STARTING lifecycleState = STARTING
super.start() super.start()
devices.values.forEach { devices.values.forEach {
it.start() it.start()
} }
lifecycleState = DeviceLifecycleState.STARTED lifecycleState = STARTED
} }
@OptIn(DFExperimental::class) @OptIn(DFExperimental::class)
@ -141,7 +155,7 @@ public class DeviceGroup(
it.stop() it.stop()
} }
super.stop() super.stop()
lifecycleState = DeviceLifecycleState.STOPPED lifecycleState = STOPPED
} }
public companion object { public companion object {
@ -149,7 +163,7 @@ public class DeviceGroup(
} }
} }
public fun DeviceManager.deviceGroup( public fun DeviceManager.registerDeviceGroup(
name: String = "@group", name: String = "@group",
meta: Meta = Meta.EMPTY, meta: Meta = Meta.EMPTY,
block: DeviceGroup.() -> Unit, block: DeviceGroup.() -> Unit,
@ -159,11 +173,11 @@ public fun DeviceManager.deviceGroup(
return group return group
} }
public fun Context.deviceGroup( public fun Context.registerDeviceGroup(
name: String = "@group", name: String = "@group",
meta: Meta = Meta.EMPTY, meta: Meta = Meta.EMPTY,
block: DeviceGroup.() -> Unit, block: DeviceGroup.() -> Unit,
): DeviceGroup = request(DeviceManager).deviceGroup(name, meta, block) ): DeviceGroup = request(DeviceManager).registerDeviceGroup(name, meta, block)
private fun DeviceGroup.getOrCreateGroup(name: Name): DeviceGroup { private fun DeviceGroup.getOrCreateGroup(name: Name): DeviceGroup {
return when (name.length) { return when (name.length) {
@ -171,7 +185,7 @@ private fun DeviceGroup.getOrCreateGroup(name: Name): DeviceGroup {
1 -> { 1 -> {
val token = name.first() val token = name.first()
when (val d = devices[token]) { when (val d = devices[token]) {
null -> device( null -> registerDevice(
token, token,
DeviceGroup(deviceManager, meta[token] ?: Meta.EMPTY) DeviceGroup(deviceManager, meta[token] ?: Meta.EMPTY)
) )
@ -187,79 +201,99 @@ private fun DeviceGroup.getOrCreateGroup(name: Name): DeviceGroup {
/** /**
* Register a device at given [name] path * Register a device at given [name] path
*/ */
public fun <D : Device> DeviceGroup.device(name: Name, device: D): D { public fun <D : Device> DeviceGroup.registerDevice(name: Name, device: D): D {
return when (name.length) { return when (name.length) {
0 -> error("Can't use empty name for a child device") 0 -> error("Can't use empty name for a child device")
1 -> device(name.first(), device) 1 -> registerDevice(name.first(), device)
else -> getOrCreateGroup(name.cutLast()).device(name.tokens.last(), device) else -> getOrCreateGroup(name.cutLast()).registerDevice(name.tokens.last(), device)
} }
} }
public fun <D: Device> DeviceGroup.device(name: String, device: D): D = device(name.parseAsName(), device) public fun <D : Device> DeviceGroup.registerDevice(name: String, device: D): D = registerDevice(name.parseAsName(), device)
/** /**
* Add a device creating intermediate groups if necessary. If device with given [name] already exists, throws an error. * Add a device creating intermediate groups if necessary. If device with given [name] already exists, throws an error.
* @param name the name of the device in the group
* @param factory a factory used to create a device
* @param deviceMeta meta override for this specific device
* @param metaLocation location of the template meta in parent group meta
*/ */
public fun DeviceGroup.device(name: Name, factory: Factory<Device>, deviceMeta: Meta? = null): Device { public fun <D : Device> DeviceGroup.registerDevice(
val newDevice = factory.build(deviceManager.context, Laminate(deviceMeta, meta[name])) name: Name,
device(name, newDevice) factory: Factory<D>,
deviceMeta: Meta? = null,
metaLocation: Name = name,
): D {
val newDevice = factory.build(deviceManager.context, Laminate(deviceMeta, meta[metaLocation]))
registerDevice(name, newDevice)
return newDevice return newDevice
} }
public fun DeviceGroup.device( public fun <D : Device> DeviceGroup.registerDevice(
name: String, name: String,
factory: Factory<Device>, factory: Factory<D>,
metaLocation: Name = name.parseAsName(),
metaBuilder: (MutableMeta.() -> Unit)? = null, metaBuilder: (MutableMeta.() -> Unit)? = null,
): Device = device(name.parseAsName(), factory, metaBuilder?.let { Meta(it) }) ): D = registerDevice(name.parseAsName(), factory, metaBuilder?.let { Meta(it) }, metaLocation)
/** /**
* Create or edit a group with a given [name]. * Create or edit a group with a given [name].
*/ */
public fun DeviceGroup.deviceGroup(name: Name, block: DeviceGroup.() -> Unit): DeviceGroup = public fun DeviceGroup.registerDeviceGroup(name: Name, block: DeviceGroup.() -> Unit): DeviceGroup =
getOrCreateGroup(name).apply(block) getOrCreateGroup(name).apply(block)
public fun DeviceGroup.deviceGroup(name: String, block: DeviceGroup.() -> Unit): DeviceGroup = public fun DeviceGroup.registerDeviceGroup(name: String, block: DeviceGroup.() -> Unit): DeviceGroup =
deviceGroup(name.parseAsName(), block) registerDeviceGroup(name.parseAsName(), block)
public fun <T : Any> DeviceGroup.property( /**
* Register read-only property based on [state]
*/
public fun <T : Any> DeviceGroup.registerProperty(
name: String, name: String,
state: DeviceState<T>, state: DeviceState<T>,
descriptorBuilder: PropertyDescriptor.() -> Unit = {}, descriptorBuilder: PropertyDescriptor.() -> Unit = {},
): DeviceState<T> { ) {
property( registerProperty(
PropertyDescriptor(name).apply(descriptorBuilder), PropertyDescriptor(name).apply(descriptorBuilder),
state state
) )
return state
} }
public fun <T : Any> DeviceGroup.mutableProperty( /**
* Register a mutable property based on mutable [state]
*/
public fun <T : Any> DeviceGroup.registerMutableProperty(
name: String, name: String,
state: MutableDeviceState<T>, state: MutableDeviceState<T>,
descriptorBuilder: PropertyDescriptor.() -> Unit = {}, descriptorBuilder: PropertyDescriptor.() -> Unit = {},
): MutableDeviceState<T> { ) {
property( registerProperty(
PropertyDescriptor(name).apply(descriptorBuilder), PropertyDescriptor(name).apply(descriptorBuilder),
state state
) )
return state
} }
public fun <T : Any> DeviceGroup.virtualProperty(
name: String,
initialValue: T,
converter: MetaConverter<T>,
descriptorBuilder: PropertyDescriptor.() -> Unit = {},
): MutableDeviceState<T> {
val state = VirtualDeviceState<T>(converter, initialValue)
return mutableProperty(name, state, descriptorBuilder)
}
/** /**
* Create a virtual [MutableDeviceState], but do not register it to a device * Create a virtual [MutableDeviceState], but do not register it to a device
*/ */
@Suppress("UnusedReceiverParameter") @Suppress("UnusedReceiverParameter")
public fun <T : Any> DeviceGroup.standAloneProperty( 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.
* @return the mutable state used in property
*/
public fun <T : Any> DeviceGroup.registerVirtualProperty(
name: String,
initialValue: T, initialValue: T,
converter: MetaConverter<T>, converter: MetaConverter<T>,
): MutableDeviceState<T> = VirtualDeviceState<T>(converter, initialValue) descriptorBuilder: PropertyDescriptor.() -> Unit = {},
): MutableDeviceState<T> {
val state = state(converter, initialValue)
registerMutableProperty(name, state, descriptorBuilder)
return state
}

View File

@ -1,5 +1,7 @@
package space.kscience.controls.constructor package space.kscience.controls.constructor
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import space.kscience.controls.api.Device import space.kscience.controls.api.Device
@ -9,6 +11,7 @@ import space.kscience.controls.spec.MutableDevicePropertySpec
import space.kscience.controls.spec.name import space.kscience.controls.spec.name
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.time.Duration
/** /**
* An observable state of a device * An observable state of a device
@ -18,6 +21,8 @@ public interface DeviceState<T> {
public val value: T public val value: T
public val valueFlow: Flow<T> public val valueFlow: Flow<T>
public companion object
} }
public val <T> DeviceState<T>.metaFlow: Flow<Meta> get() = valueFlow.map(converter::objectToMeta) public val <T> DeviceState<T>.metaFlow: Flow<Meta> get() = valueFlow.map(converter::objectToMeta)
@ -55,7 +60,7 @@ private open class BoundDeviceState<T>(
override val converter: MetaConverter<T>, override val converter: MetaConverter<T>,
val device: Device, val device: Device,
val propertyName: String, val propertyName: String,
private val initialValue: T, initialValue: T,
) : DeviceState<T> { ) : DeviceState<T> {
override val valueFlow: StateFlow<T> = device.messageFlow.filterIsInstance<PropertyChangedMessage>().filter { override val valueFlow: StateFlow<T> = device.messageFlow.filterIsInstance<PropertyChangedMessage>().filter {
@ -121,3 +126,58 @@ public suspend fun <D : Device, T> D.bindMutableStateToProperty(
): MutableDeviceState<T> = bindMutableStateToProperty(propertySpec.name, propertySpec.converter) ): MutableDeviceState<T> = bindMutableStateToProperty(propertySpec.name, propertySpec.converter)
private open class ExternalState<T>(
val scope: CoroutineScope,
override val converter: MetaConverter<T>,
val readInterval: Duration,
initialValue: T,
val reader: suspend () -> T,
) : DeviceState<T> {
protected val flow: StateFlow<T> = flow {
while (true) {
delay(readInterval)
emit(reader())
}
}.stateIn(scope, SharingStarted.Eagerly, initialValue)
override val value: T get() = flow.value
override val valueFlow: Flow<T> get() = flow
}
/**
* Create a [DeviceState] which is constructed by periodically reading external value
*/
public fun <T> DeviceState.Companion.external(
scope: CoroutineScope,
converter: MetaConverter<T>,
readInterval: Duration,
initialValue: T,
reader: suspend () -> T,
): DeviceState<T> = ExternalState(scope, converter, readInterval, initialValue, reader)
private class MutableExternalState<T>(
scope: CoroutineScope,
converter: MetaConverter<T>,
readInterval: Duration,
initialValue: T,
reader: suspend () -> T,
val writer: suspend (T) -> Unit,
) : ExternalState<T>(scope, converter, readInterval, initialValue, reader), MutableDeviceState<T> {
override var value: T
get() = super.value
set(value) {
scope.launch {
writer(value)
}
}
}
public fun <T> DeviceState.Companion.external(
scope: CoroutineScope,
converter: MetaConverter<T>,
readInterval: Duration,
initialValue: T,
reader: suspend () -> T,
writer: suspend (T) -> Unit,
): MutableDeviceState<T> = MutableExternalState(scope, converter, readInterval, initialValue, reader, writer)

View File

@ -8,6 +8,7 @@ import space.kscience.controls.api.Device
import space.kscience.controls.manager.clock import space.kscience.controls.manager.clock
import space.kscience.controls.spec.* import space.kscience.controls.spec.*
import space.kscience.dataforge.context.Context import space.kscience.dataforge.context.Context
import space.kscience.dataforge.context.Factory
import space.kscience.dataforge.meta.double import space.kscience.dataforge.meta.double
import space.kscience.dataforge.meta.get import space.kscience.dataforge.meta.get
import space.kscience.dataforge.meta.transformations.MetaConverter import space.kscience.dataforge.meta.transformations.MetaConverter
@ -84,12 +85,15 @@ public class VirtualDrive(
override fun onStop() { override fun onStop() {
updateJob?.cancel() updateJob?.cancel()
} }
public companion object {
public fun factory(
mass: Double,
positionState: MutableDeviceState<Double>,
): Factory<Drive> = Factory { context, _ ->
VirtualDrive(context, mass, positionState)
}
}
} }
public suspend fun Drive.stateOfForce(): MutableDeviceState<Double> = bindMutableStateToProperty(Drive.force) public suspend fun Drive.stateOfForce(): MutableDeviceState<Double> = bindMutableStateToProperty(Drive.force)
public fun DeviceGroup.virtualDrive(
name: String,
mass: Double,
positionState: MutableDeviceState<Double>,
): VirtualDrive = device(name, VirtualDrive(context, mass, positionState))

View File

@ -8,7 +8,7 @@ import space.kscience.controls.spec.DevicePropertySpec
import space.kscience.controls.spec.DeviceSpec import space.kscience.controls.spec.DeviceSpec
import space.kscience.controls.spec.booleanProperty import space.kscience.controls.spec.booleanProperty
import space.kscience.dataforge.context.Context import space.kscience.dataforge.context.Context
import space.kscience.dataforge.names.parseAsName import space.kscience.dataforge.context.Factory
/** /**
@ -20,6 +20,9 @@ public interface LimitSwitch : Device {
public companion object : DeviceSpec<LimitSwitch>() { public companion object : DeviceSpec<LimitSwitch>() {
public val locked: DevicePropertySpec<LimitSwitch, Boolean> by booleanProperty { locked } public val locked: DevicePropertySpec<LimitSwitch, Boolean> by booleanProperty { locked }
public fun factory(lockedState: DeviceState<Boolean>): Factory<LimitSwitch> = Factory { context, _ ->
VirtualLimitSwitch(context, lockedState)
}
} }
} }
@ -39,6 +42,3 @@ public class VirtualLimitSwitch(
override val locked: Boolean get() = lockedState.value override val locked: Boolean get() = lockedState.value
} }
public fun DeviceGroup.virtualLimitSwitch(name: String, lockedState: DeviceState<Boolean>): VirtualLimitSwitch =
device(name.parseAsName(), VirtualLimitSwitch(context, lockedState))

View File

@ -79,4 +79,4 @@ public fun DeviceGroup.pid(
name: String, name: String,
drive: Drive, drive: Drive,
pidParameters: PidParameters, pidParameters: PidParameters,
): PidRegulator = device(name, PidRegulator(drive, pidParameters)) ): PidRegulator = registerDevice(name, PidRegulator(drive, pidParameters))

View File

@ -39,3 +39,9 @@ public class DoubleRangeState(
*/ */
public val atEndState: DeviceState<Boolean> = map(MetaConverter.boolean) { it >= range.endInclusive } public val atEndState: DeviceState<Boolean> = map(MetaConverter.boolean) { it >= range.endInclusive }
} }
@Suppress("UnusedReceiverParameter")
public fun DeviceGroup.rangeState(
initialValue: Double,
range: ClosedFloatingPointRange<Double>,
): DoubleRangeState = DoubleRangeState(initialValue, range)

View File

@ -147,7 +147,7 @@ internal class MetaStructureCodec(
"Float" -> member.value?.numberOrNull?.toFloat() "Float" -> member.value?.numberOrNull?.toFloat()
"Double" -> member.value?.numberOrNull?.toDouble() "Double" -> member.value?.numberOrNull?.toDouble()
"String" -> member.string "String" -> member.string
"DateTime" -> DateTime(member.instant.toJavaInstant()) "DateTime" -> member.instant?.toJavaInstant()?.let { DateTime(it) }
"Guid" -> member.string?.let { UUID.fromString(it) } "Guid" -> member.string?.let { UUID.fromString(it) }
"ByteString" -> member.value?.list?.let { list -> "ByteString" -> member.value?.list?.let { list ->
ByteString(list.map { it.number.toByte() }.toByteArray()) ByteString(list.map { it.number.toByte() }.toByteArray())

View File

@ -10,7 +10,7 @@ description = """
val visionforgeVersion = "0.3.0-dev-10" val visionforgeVersion = "0.3.0-dev-10"
kscience { kscience {
fullStack("js/controls-vision.js", development = true) fullStack("js/controls-vision.js")
useKtor() useKtor()
useContextReceivers() useContextReceivers()
dependencies { dependencies {

View File

@ -37,11 +37,11 @@ public fun main() {
timeStep = 0.005.seconds timeStep = 0.005.seconds
) )
val device = context.deviceGroup { val device = context.registerDeviceGroup {
val drive = virtualDrive("drive", 0.005, state) val drive = VirtualDrive(context, 0.005, state)
val pid = pid("pid", drive, pidParameters) val pid = pid("pid", drive, pidParameters)
virtualLimitSwitch("start", state.atStartState) registerDevice("start", LimitSwitch.factory(state.atStartState))
virtualLimitSwitch("end", state.atEndState) registerDevice("end", LimitSwitch.factory(state.atEndState))
val clock = context.clock val clock = context.clock
val clockStart = clock.now() val clockStart = clock.now()

View File

@ -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-8.3-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists