diff --git a/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/api/Device.kt b/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/api/Device.kt index 230872d..c64af12 100644 --- a/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/api/Device.kt +++ b/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/api/Device.kt @@ -47,7 +47,7 @@ public interface Device : Closeable, ContextAware, CoroutineScope { * Set property [value] for a property with name [propertyName]. * In rare cases could suspend if the [Device] supports command queue and it is full at the moment. */ - public suspend fun writeItem(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 diff --git a/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/api/DeviceHub.kt b/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/api/DeviceHub.kt index 16d5970..aba8517 100644 --- a/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/api/DeviceHub.kt +++ b/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/api/DeviceHub.kt @@ -59,8 +59,8 @@ public operator fun DeviceHub.get(nameString: String): Device = public suspend fun DeviceHub.readProperty(deviceName: Name, propertyName: String): Meta = this[deviceName].readProperty(propertyName) -public suspend fun DeviceHub.writeItem(deviceName: Name, propertyName: String, value: Meta) { - this[deviceName].writeItem(propertyName, value) +public suspend fun DeviceHub.writeProperty(deviceName: Name, propertyName: String, value: Meta) { + this[deviceName].writeProperty(propertyName, value) } public suspend fun DeviceHub.execute(deviceName: Name, command: String, argument: Meta?): Meta? = diff --git a/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/base/DeviceBase.kt b/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/base/DeviceBase.kt index 7b4d59c..52d37bf 100644 --- a/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/base/DeviceBase.kt +++ b/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/base/DeviceBase.kt @@ -164,7 +164,7 @@ public abstract class DeviceBase(final override val context: Context) : Device { (_properties[propertyName] ?: error("Property with name $propertyName not defined")).invalidate() } - override suspend fun writeItem(propertyName: String, value: Meta) { + override suspend fun writeProperty(propertyName: String, value: Meta) { (_properties[propertyName] as? DeviceProperty ?: error("Property with name $propertyName not defined")).write( value ) diff --git a/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/controllers/deviceMessages.kt b/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/controllers/deviceMessages.kt index bd71182..75781e6 100644 --- a/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/controllers/deviceMessages.kt +++ b/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/controllers/deviceMessages.kt @@ -25,7 +25,7 @@ public suspend fun Device.respondMessage(deviceTarget: Name, request: DeviceMess if (request.value == null) { invalidate(request.property) } else { - writeItem(request.property, request.value) + writeProperty(request.property, request.value) } PropertyChangedMessage( property = request.property, diff --git a/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/properties/DeviceBySpec.kt b/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/properties/DeviceBySpec.kt index 91f0d61..2d8ce2a 100644 --- a/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/properties/DeviceBySpec.kt +++ b/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/properties/DeviceBySpec.kt @@ -11,11 +11,7 @@ import ru.mipt.npm.controls.api.* import space.kscience.dataforge.context.Context import space.kscience.dataforge.context.Global import space.kscience.dataforge.meta.Meta -import space.kscience.dataforge.meta.transformations.MetaConverter import kotlin.coroutines.CoroutineContext -import kotlin.properties.Delegates.observable -import kotlin.properties.ReadWriteProperty -import kotlin.reflect.KProperty /** * A device generated from specification @@ -58,7 +54,7 @@ public open class DeviceBySpec>( private val stateLock = Mutex() - private suspend fun updateLogical(propertyName: String, value: Meta?) { + protected suspend fun updateLogical(propertyName: String, value: Meta?) { if (value != logicalState[propertyName]) { stateLock.withLock { logicalState[propertyName] = value @@ -74,7 +70,7 @@ public open class DeviceBySpec>( * The logical state is updated after read */ override suspend fun readProperty(propertyName: String): Meta { - val newValue = properties[propertyName]?.readItem(self) + val newValue = properties[propertyName]?.readMeta(self) ?: error("A property with name $propertyName is not registered in $this") updateLogical(propertyName, newValue) return newValue @@ -88,10 +84,10 @@ public open class DeviceBySpec>( } } - override suspend fun writeItem(propertyName: String, value: Meta): Unit { + override suspend fun writeProperty(propertyName: String, value: Meta): Unit { //If there is a physical property with given name, invalidate logical property and write physical one - (properties[propertyName] as? WritableDevicePropertySpec)?.let { - it.writeItem(self, value) + (properties[propertyName] as? WritableDevicePropertySpec)?.let { + it.writeMeta(self, value) invalidate(propertyName) } ?: run { updateLogical(propertyName, value) @@ -99,39 +95,23 @@ public open class DeviceBySpec>( } override suspend fun execute(action: String, argument: Meta?): Meta? = - actions[action]?.executeItem(self, argument) - - - /** - * A delegate that represents the logical-only state of the device - */ - public fun state( - converter: MetaConverter, - initialValue: T, - ): ReadWriteProperty = observable(initialValue) { property: KProperty<*>, oldValue: T, newValue: T -> - if (oldValue != newValue) { - launch { - invalidate(property.name) - sharedMessageFlow.emit(PropertyChangedMessage(property.name, converter.objectToMeta(newValue))) - } - } - } + actions[action]?.executeMeta(self, argument) /** * Read typed value and update/push event if needed */ - public suspend fun DevicePropertySpec.read(): T { + public suspend fun DevicePropertySpec.read(): T { val res = read(self) updateLogical(name, converter.objectToMeta(res)) return res } - public fun DevicePropertySpec.get(): T? = getProperty(name)?.let(converter::metaToObject) + public fun DevicePropertySpec.get(): T? = getProperty(name)?.let(converter::metaToObject) /** * Write typed property state and invalidate logical state */ - public suspend fun WritableDevicePropertySpec.write(value: T) { + public suspend fun WritableDevicePropertySpec.write(value: T) { write(self, value) invalidate(name) } @@ -146,7 +126,7 @@ public suspend fun , T : Any> D.read( propertySpec: DevicePropertySpec ): T = propertySpec.read() -public fun , T : Any> D.write( +public fun , T> D.write( propertySpec: WritableDevicePropertySpec, value: T ): Job = launch { diff --git a/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/properties/DevicePropertySpec.kt b/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/properties/DevicePropertySpec.kt index 5184d1d..23ceffb 100644 --- a/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/properties/DevicePropertySpec.kt +++ b/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/properties/DevicePropertySpec.kt @@ -5,8 +5,6 @@ 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 -import space.kscience.dataforge.meta.transformations.nullableMetaToObject -import space.kscience.dataforge.meta.transformations.nullableObjectToMeta /** @@ -15,8 +13,7 @@ import space.kscience.dataforge.meta.transformations.nullableObjectToMeta @RequiresOptIn public annotation class InternalDeviceAPI -//TODO relax T restriction after DF 0.4.4 -public interface DevicePropertySpec { +public interface DevicePropertySpec { /** * Property name, should be unique in device */ @@ -40,11 +37,11 @@ public interface DevicePropertySpec { } @OptIn(InternalDeviceAPI::class) -public suspend fun DevicePropertySpec.readItem(device: D): Meta = +public suspend fun DevicePropertySpec.readMeta(device: D): Meta = converter.objectToMeta(read(device)) -public interface WritableDevicePropertySpec : DevicePropertySpec { +public interface WritableDevicePropertySpec : DevicePropertySpec { /** * Write physical value to a device */ @@ -53,11 +50,11 @@ public interface WritableDevicePropertySpec : DeviceProp } @OptIn(InternalDeviceAPI::class) -public suspend fun WritableDevicePropertySpec.writeItem(device: D, item: Meta) { +public suspend fun WritableDevicePropertySpec.writeMeta(device: D, item: Meta) { write(device, converter.metaToObject(item) ?: error("Meta $item could not be read with $converter")) } -public interface DeviceActionSpec { +public interface DeviceActionSpec { /** * Action name, should be unique in device */ @@ -78,11 +75,11 @@ public interface DeviceActionSpec { public suspend fun execute(device: D, input: I?): O? } -public suspend fun DeviceActionSpec.executeItem( +public suspend fun DeviceActionSpec.executeMeta( device: D, item: Meta? ): Meta? { - val arg = inputConverter.nullableMetaToObject(item) + val arg = item?.let { inputConverter.metaToObject(item) } val res = execute(device, arg) - return outputConverter.nullableObjectToMeta(res) + return res?.let { outputConverter.objectToMeta(res) } } \ No newline at end of file diff --git a/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/properties/DeviceSpec.kt b/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/properties/DeviceSpec.kt index 17d52c7..35e59ba 100644 --- a/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/properties/DeviceSpec.kt +++ b/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/properties/DeviceSpec.kt @@ -52,8 +52,9 @@ public abstract class DeviceSpec>( override val name: String = readWriteProperty.name override val descriptor: PropertyDescriptor = PropertyDescriptor(this.name).apply(descriptorBuilder) override val converter: MetaConverter = converter - override suspend fun read(device: D): T = - withContext(device.coroutineContext) { readWriteProperty.get(device) } + override suspend fun read(device: D): T = withContext(device.coroutineContext) { + readWriteProperty.get(device) + } override suspend fun write(device: D, value: T) = withContext(device.coroutineContext) { readWriteProperty.set(device, value) @@ -145,12 +146,12 @@ public abstract class DeviceSpec>( /** * The function is executed right after device initialization is finished */ - public open fun D.onStartup(){} + public open fun D.onStartup() {} /** * The function is executed before device is shut down */ - public open fun D.onShutdown(){} + public open fun D.onShutdown() {} override fun invoke(meta: Meta, context: Context): D = buildDevice().apply { diff --git a/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/properties/deviceStateDelegates.kt b/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/properties/deviceStateDelegates.kt deleted file mode 100644 index f3f4c69..0000000 --- a/controls-core/src/commonMain/kotlin/ru/mipt/npm/controls/properties/deviceStateDelegates.kt +++ /dev/null @@ -1,12 +0,0 @@ -package ru.mipt.npm.controls.properties - -import space.kscience.dataforge.meta.transformations.MetaConverter -import kotlin.properties.ReadWriteProperty - -public fun > D.state( - initialValue: Double, -): ReadWriteProperty = state(MetaConverter.double, initialValue) - -public fun > D.state( - initialValue: Number, -): ReadWriteProperty = state(MetaConverter.number, initialValue) \ No newline at end of file diff --git a/controls-magix-client/src/commonMain/kotlin/ru/mipt/npm/controls/client/tangoMagix.kt b/controls-magix-client/src/commonMain/kotlin/ru/mipt/npm/controls/client/tangoMagix.kt index e04a7ed..239089b 100644 --- a/controls-magix-client/src/commonMain/kotlin/ru/mipt/npm/controls/client/tangoMagix.kt +++ b/controls-magix-client/src/commonMain/kotlin/ru/mipt/npm/controls/client/tangoMagix.kt @@ -94,7 +94,7 @@ public fun DeviceManager.launchTangoMagix( } TangoAction.write -> { request.payload.value?.let { value -> - device.writeItem(request.payload.name, value) + device.writeProperty(request.payload.name, value) } //wait for value to be written and return final state val value = device.getOrReadItem(request.payload.name) diff --git a/controls-opcua/build.gradle.kts b/controls-opcua/build.gradle.kts index caea7f5..c3874f2 100644 --- a/controls-opcua/build.gradle.kts +++ b/controls-opcua/build.gradle.kts @@ -8,6 +8,7 @@ val miloVersion: String = "0.6.3" dependencies { api(project(":controls-core")) + api("org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:${ru.mipt.npm.gradle.KScienceVersions.coroutinesVersion}") implementation("org.eclipse.milo:sdk-client:$miloVersion") implementation("org.eclipse.milo:bsd-parser:$miloVersion") implementation("org.eclipse.milo:dictionary-reader:$miloVersion") diff --git a/controls-opcua/src/main/kotlin/ru/mipt/npm/controls/opcua/MiloDevice.kt b/controls-opcua/src/main/kotlin/ru/mipt/npm/controls/opcua/MiloDevice.kt index ae79425..7ea36de 100644 --- a/controls-opcua/src/main/kotlin/ru/mipt/npm/controls/opcua/MiloDevice.kt +++ b/controls-opcua/src/main/kotlin/ru/mipt/npm/controls/opcua/MiloDevice.kt @@ -1,16 +1,12 @@ package ru.mipt.npm.controls.opcua +import kotlinx.coroutines.future.await import org.eclipse.milo.opcua.sdk.client.OpcUaClient -import org.eclipse.milo.opcua.stack.core.types.builtin.DataValue -import org.eclipse.milo.opcua.stack.core.types.builtin.ExtensionObject -import org.eclipse.milo.opcua.stack.core.types.builtin.NodeId -import org.eclipse.milo.opcua.stack.core.types.builtin.Variant +import org.eclipse.milo.opcua.stack.core.types.builtin.* import org.eclipse.milo.opcua.stack.core.types.enumerated.TimestampsToReturn import ru.mipt.npm.controls.api.Device import space.kscience.dataforge.meta.Meta import space.kscience.dataforge.meta.transformations.MetaConverter -import kotlin.properties.ReadWriteProperty -import kotlin.reflect.KProperty /** @@ -28,40 +24,46 @@ public interface MiloDevice : Device { } } -public inline fun MiloDevice.opc( +public suspend inline fun MiloDevice.readOpcWithTime( nodeId: NodeId, converter: MetaConverter, magAge: Double = 500.0 -): ReadWriteProperty = object : ReadWriteProperty { - override fun getValue(thisRef: Any?, property: KProperty<*>): T { - val data = client.readValue(magAge, TimestampsToReturn.Server, nodeId).get() - val meta: Meta = when (val content = data.value.value) { - is T -> return content - content is Meta -> content as Meta - content is ExtensionObject -> (content as ExtensionObject).decode(client.dynamicSerializationContext) as Meta - else -> error("Incompatible OPC property value $content") - } - - return converter.metaToObject(meta) ?: error("Meta $meta could not be converted to ${T::class}") +): Pair { + val data = client.readValue(magAge, TimestampsToReturn.Server, nodeId).await() + val time = data.serverTime ?: error("No server time provided") + val meta: Meta = when (val content = data.value.value) { + is T -> return content to time + content is Meta -> content as Meta + content is ExtensionObject -> (content as ExtensionObject).decode(client.dynamicSerializationContext) as Meta + else -> error("Incompatible OPC property value $content") } - override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) { - val meta = converter.objectToMeta(value) - client.writeValue(nodeId, DataValue(Variant(meta))) - } + val res = converter.metaToObject(meta) ?: error("Meta $meta could not be converted to ${T::class}") + return res to time } -public inline fun MiloDevice.opcDouble( +public suspend inline fun MiloDevice.readOpc( nodeId: NodeId, - magAge: Double = 1.0 -): ReadWriteProperty = opc(nodeId, MetaConverter.double, magAge) + converter: MetaConverter, + magAge: Double = 500.0 +): T { + val data = client.readValue(magAge, TimestampsToReturn.Neither, nodeId).await() -public inline fun MiloDevice.opcInt( - nodeId: NodeId, - magAge: Double = 1.0 -): ReadWriteProperty = opc(nodeId, MetaConverter.int, magAge) + val meta: Meta = when (val content = data.value.value) { + is T -> return content + content is Meta -> content as Meta + content is ExtensionObject -> (content as ExtensionObject).decode(client.dynamicSerializationContext) as Meta + else -> error("Incompatible OPC property value $content") + } -public inline fun MiloDevice.opcString( + return converter.metaToObject(meta) ?: error("Meta $meta could not be converted to ${T::class}") +} + +public suspend inline fun MiloDevice.writeOpc( nodeId: NodeId, - magAge: Double = 1.0 -): ReadWriteProperty = opc(nodeId, MetaConverter.string, magAge) \ No newline at end of file + converter: MetaConverter, + value: T +): StatusCode { + val meta = converter.objectToMeta(value) + return client.writeValue(nodeId, DataValue(Variant(meta))).await() +} \ No newline at end of file diff --git a/controls-opcua/src/main/kotlin/ru/mipt/npm/controls/opcua/MiloDeviceBySpec.kt b/controls-opcua/src/main/kotlin/ru/mipt/npm/controls/opcua/MiloDeviceBySpec.kt index 6b98de2..b60498e 100644 --- a/controls-opcua/src/main/kotlin/ru/mipt/npm/controls/opcua/MiloDeviceBySpec.kt +++ b/controls-opcua/src/main/kotlin/ru/mipt/npm/controls/opcua/MiloDeviceBySpec.kt @@ -1,6 +1,9 @@ package ru.mipt.npm.controls.opcua +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking import org.eclipse.milo.opcua.sdk.client.OpcUaClient +import org.eclipse.milo.opcua.stack.core.types.builtin.NodeId import ru.mipt.npm.controls.properties.DeviceBySpec import ru.mipt.npm.controls.properties.DeviceSpec import space.kscience.dataforge.context.Context @@ -8,12 +11,15 @@ import space.kscience.dataforge.context.Global import space.kscience.dataforge.meta.Meta import space.kscience.dataforge.meta.get import space.kscience.dataforge.meta.string +import space.kscience.dataforge.meta.transformations.MetaConverter +import kotlin.properties.ReadWriteProperty +import kotlin.reflect.KProperty -public open class MiloDeviceBySpec>( +public open class MiloDeviceBySpec>( spec: DeviceSpec, context: Context = Global, meta: Meta = Meta.EMPTY -): MiloDevice, DeviceBySpec(spec, context, meta) { +) : MiloDevice, DeviceBySpec(spec, context, meta) { override val client: OpcUaClient by lazy { val endpointUrl = meta["endpointUrl"].string ?: error("Endpoint url is not defined") @@ -26,4 +32,38 @@ public open class MiloDeviceBySpec>( super.close() super.close() } -} \ No newline at end of file +} + +/** + * A device-bound OPC-UA property. Does not trigger device properties change. + */ +public inline fun MiloDeviceBySpec<*>.opc( + nodeId: NodeId, + converter: MetaConverter, + magAge: Double = 500.0 +): ReadWriteProperty = object : ReadWriteProperty { + override fun getValue(thisRef: Any?, property: KProperty<*>): T = runBlocking { + readOpc(nodeId, converter, magAge) + } + + override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) { + launch { + writeOpc(nodeId, converter, value) + } + } +} + +public inline fun MiloDeviceBySpec<*>.opcDouble( + nodeId: NodeId, + magAge: Double = 1.0 +): ReadWriteProperty = opc(nodeId, MetaConverter.double, magAge) + +public inline fun MiloDeviceBySpec<*>.opcInt( + nodeId: NodeId, + magAge: Double = 1.0 +): ReadWriteProperty = opc(nodeId, MetaConverter.int, magAge) + +public inline fun MiloDeviceBySpec<*>.opcString( + nodeId: NodeId, + magAge: Double = 1.0 +): ReadWriteProperty = opc(nodeId, MetaConverter.string, magAge) \ No newline at end of file diff --git a/demo/src/main/kotlin/ru/mipt/npm/controls/demo/DemoControllerView.kt b/demo/src/main/kotlin/ru/mipt/npm/controls/demo/DemoControllerView.kt index 4ef7d4a..4f61194 100644 --- a/demo/src/main/kotlin/ru/mipt/npm/controls/demo/DemoControllerView.kt +++ b/demo/src/main/kotlin/ru/mipt/npm/controls/demo/DemoControllerView.kt @@ -10,6 +10,9 @@ import ru.mipt.npm.controls.api.DeviceMessage import ru.mipt.npm.controls.client.connectToMagix import ru.mipt.npm.controls.controllers.DeviceManager import ru.mipt.npm.controls.controllers.install +import ru.mipt.npm.controls.demo.DemoDevice.Companion.cosScale +import ru.mipt.npm.controls.demo.DemoDevice.Companion.sinScale +import ru.mipt.npm.controls.demo.DemoDevice.Companion.timeScale import ru.mipt.npm.magix.api.MagixEndpoint import ru.mipt.npm.magix.rsocket.rSocketWithTcp import ru.mipt.npm.magix.rsocket.rSocketWithWebSockets @@ -97,10 +100,12 @@ class DemoControllerView : View(title = " Demo controller remote") { button("Submit") { useMaxWidth = true action { - controller.device?.apply { - timeScale = timeScaleSlider.value - sinScale = xScaleSlider.value - cosScale = yScaleSlider.value + controller.device?.run { + launch { + timeScale.write(timeScaleSlider.value) + sinScale.write(xScaleSlider.value) + cosScale.write(yScaleSlider.value) + } } } } diff --git a/demo/src/main/kotlin/ru/mipt/npm/controls/demo/DemoDevice.kt b/demo/src/main/kotlin/ru/mipt/npm/controls/demo/DemoDevice.kt index 308bc48..8d88199 100644 --- a/demo/src/main/kotlin/ru/mipt/npm/controls/demo/DemoDevice.kt +++ b/demo/src/main/kotlin/ru/mipt/npm/controls/demo/DemoDevice.kt @@ -9,24 +9,24 @@ import kotlin.time.ExperimentalTime class DemoDevice : DeviceBySpec(DemoDevice) { - var timeScale by state(5000.0) - var sinScale by state(1.0) - var cosScale by state(1.0) + private var timeScaleState = 5000.0 + private var sinScaleState = 1.0 + private var cosScaleState = 1.0 companion object : DeviceSpec(::DemoDevice) { // register virtual properties based on actual object state - val timeScaleProperty = registerProperty(MetaConverter.double, DemoDevice::timeScale) - val sinScaleProperty = registerProperty(MetaConverter.double, DemoDevice::sinScale) - val cosScaleProperty = registerProperty(MetaConverter.double, DemoDevice::cosScale) + val timeScale = registerProperty(MetaConverter.double, DemoDevice::timeScaleState) + val sinScale = registerProperty(MetaConverter.double, DemoDevice::sinScaleState) + val cosScale = registerProperty(MetaConverter.double, DemoDevice::cosScaleState) val sin by doubleProperty { val time = Instant.now() - kotlin.math.sin(time.toEpochMilli().toDouble() / timeScale) * sinScale + kotlin.math.sin(time.toEpochMilli().toDouble() / timeScaleState) * sinScaleState } val cos by doubleProperty { val time = Instant.now() - kotlin.math.cos(time.toEpochMilli().toDouble() / timeScale) * sinScale + kotlin.math.cos(time.toEpochMilli().toDouble() / timeScaleState) * sinScaleState } val coordinates by metaProperty { @@ -39,9 +39,9 @@ class DemoDevice : DeviceBySpec(DemoDevice) { } val resetScale by action(MetaConverter.meta, MetaConverter.meta) { - timeScale = 5000.0 - sinScale = 1.0 - cosScale = 1.0 + timeScale.write(5000.0) + sinScale.write(1.0) + cosScale.write(1.0) null }