Compare commits

...

5 Commits

27 changed files with 164 additions and 103 deletions
build.gradle.kts
controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor
controls-core
build.gradle.kts
src
commonMain/kotlin/space/kscience/controls
jvmMain/kotlin/space/kscience/controls/ports
jvmTest/kotlin/space/kscience/controls/ports
controls-modbus/src/main/kotlin/space/kscience/controls/modbus
controls-opcua/src/main/kotlin/space/kscience/controls/opcua/client
controls-vision/src/jsMain/kotlin
demo
all-things/src/main/kotlin/space/kscience/controls/demo
car/src/main/kotlin/space/kscience/controls/demo/car
constructor
mks-pdr900/src/main/kotlin/center/sciprog/devices/mks
gradle.properties
magix

@ -5,15 +5,15 @@ plugins {
id("space.kscience.gradle.project") id("space.kscience.gradle.project")
} }
val dataforgeVersion: String by extra("0.6.2") val dataforgeVersion: String by extra("0.7.1")
val visionforgeVersion by extra("0.3.0-dev-14") val visionforgeVersion by extra("0.3.0-RC")
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-2" version = "0.3.0-dev-4"
repositories{ repositories{
maven("https://maven.pkg.jetbrains.space/spc/p/sci/dev") maven("https://maven.pkg.jetbrains.space/spc/p/sci/dev")
} }

@ -40,7 +40,7 @@ public interface MutableDeviceState<T> : DeviceState<T> {
public var <T : Any> MutableDeviceState<T>.valueAsMeta: Meta public var <T : Any> MutableDeviceState<T>.valueAsMeta: Meta
get() = converter.objectToMeta(value) get() = converter.objectToMeta(value)
set(arg) { set(arg) {
value = converter.metaToObject(arg) ?: error("Conversion for meta $arg to property type with $converter failed") value = converter.metaToObject(arg)
} }
/** /**

@ -20,10 +20,14 @@ kscience {
json() json()
} }
useContextReceivers() useContextReceivers()
dependencies { commonMain {
api("space.kscience:dataforge-io:$dataforgeVersion") api("space.kscience:dataforge-io:$dataforgeVersion")
api(spclibs.kotlinx.datetime) api(spclibs.kotlinx.datetime)
} }
jvmTest{
implementation(spclibs.logback.classic)
}
} }

@ -11,8 +11,8 @@ 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.misc.DFExperimental import space.kscience.dataforge.misc.DFExperimental
import space.kscience.dataforge.misc.Type import space.kscience.dataforge.misc.DfType
import space.kscience.dataforge.names.Name import space.kscience.dataforge.names.parseAsName
/** /**
* A lifecycle state of a device * A lifecycle state of a device
@ -46,7 +46,7 @@ public enum class DeviceLifecycleState {
* [Device] is a supervisor scope encompassing all operations on a device. * [Device] is a supervisor scope encompassing all operations on a device.
* When canceled, cancels all running processes. * When canceled, cancels all running processes.
*/ */
@Type(DEVICE_TARGET) @DfType(DEVICE_TARGET)
public interface Device : ContextAware, CoroutineScope { public interface Device : ContextAware, CoroutineScope {
/** /**
@ -144,7 +144,7 @@ public suspend fun Device.requestProperty(propertyName: String): Meta = if (this
*/ */
public fun CachingDevice.getAllProperties(): Meta = Meta { public fun CachingDevice.getAllProperties(): Meta = Meta {
for (descriptor in propertyDescriptors) { for (descriptor in propertyDescriptors) {
setMeta(Name.parse(descriptor.name), getProperty(descriptor.name)) set(descriptor.name.parseAsName(), getProperty(descriptor.name))
} }
} }

@ -1,6 +1,5 @@
package space.kscience.controls.api package space.kscience.controls.api
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
@ -9,7 +8,7 @@ import kotlinx.coroutines.launch
/** /**
* A generic bidirectional sender/receiver object * A generic bidirectional sender/receiver object
*/ */
public interface Socket<T> : Closeable { public interface Socket<T> : AutoCloseable {
/** /**
* Send an object to the socket * Send an object to the socket
*/ */

@ -1,8 +1,8 @@
package space.kscience.controls.misc package space.kscience.controls.misc
import io.ktor.utils.io.core.Input
import io.ktor.utils.io.core.Output
import kotlinx.datetime.Instant import kotlinx.datetime.Instant
import kotlinx.io.Sink
import kotlinx.io.Source
import space.kscience.dataforge.io.IOFormat import space.kscience.dataforge.io.IOFormat
import space.kscience.dataforge.meta.Meta import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.get import space.kscience.dataforge.meta.get
@ -38,15 +38,16 @@ public data class ValueWithTime<T>(val value: T, val time: Instant) {
private class ValueWithTimeIOFormat<T>(val valueFormat: IOFormat<T>) : IOFormat<ValueWithTime<T>> { private class ValueWithTimeIOFormat<T>(val valueFormat: IOFormat<T>) : IOFormat<ValueWithTime<T>> {
override val type: KType get() = typeOf<ValueWithTime<T>>() override val type: KType get() = typeOf<ValueWithTime<T>>()
override fun readObject(input: Input): ValueWithTime<T> {
val timestamp = InstantIOFormat.readObject(input) override fun readFrom(source: Source): ValueWithTime<T> {
val value = valueFormat.readObject(input) val timestamp = InstantIOFormat.readFrom(source)
val value = valueFormat.readFrom(source)
return ValueWithTime(value, timestamp) return ValueWithTime(value, timestamp)
} }
override fun writeObject(output: Output, obj: ValueWithTime<T>) { override fun writeTo(sink: Sink, obj: ValueWithTime<T>) {
InstantIOFormat.writeObject(output, obj.time) InstantIOFormat.writeTo(sink, obj.time)
valueFormat.writeObject(output, obj.value) valueFormat.writeTo(sink, obj.value)
} }
} }
@ -54,7 +55,10 @@ private class ValueWithTimeIOFormat<T>(val valueFormat: IOFormat<T>) : IOFormat<
private class ValueWithTimeMetaConverter<T>( private class ValueWithTimeMetaConverter<T>(
val valueConverter: MetaConverter<T>, val valueConverter: MetaConverter<T>,
) : MetaConverter<ValueWithTime<T>> { ) : MetaConverter<ValueWithTime<T>> {
override fun metaToObject(
override val type: KType = typeOf<ValueWithTime<T>>()
override fun metaToObjectOrNull(
meta: Meta, meta: Meta,
): ValueWithTime<T>? = valueConverter.metaToObject(meta[ValueWithTime.META_VALUE_KEY] ?: Meta.EMPTY)?.let { ): ValueWithTime<T>? = valueConverter.metaToObject(meta[ValueWithTime.META_VALUE_KEY] ?: Meta.EMPTY)?.let {
ValueWithTime(it, meta[ValueWithTime.META_TIME_KEY]?.instant ?: Instant.DISTANT_PAST) ValueWithTime(it, meta[ValueWithTime.META_TIME_KEY]?.instant ?: Instant.DISTANT_PAST)

@ -1,7 +1,9 @@
package space.kscience.controls.misc package space.kscience.controls.misc
import io.ktor.utils.io.core.*
import kotlinx.datetime.Instant import kotlinx.datetime.Instant
import kotlinx.io.Sink
import kotlinx.io.Source
import space.kscience.dataforge.context.Context import space.kscience.dataforge.context.Context
import space.kscience.dataforge.io.IOFormat import space.kscience.dataforge.io.IOFormat
import space.kscience.dataforge.io.IOFormatFactory import space.kscience.dataforge.io.IOFormatFactory
@ -23,14 +25,14 @@ public object InstantIOFormat : IOFormat<Instant>, IOFormatFactory<Instant> {
override val type: KType get() = typeOf<Instant>() override val type: KType get() = typeOf<Instant>()
override fun writeObject(output: Output, obj: Instant) { override fun writeTo(sink: Sink, obj: Instant) {
output.writeLong(obj.epochSeconds) sink.writeLong(obj.epochSeconds)
output.writeInt(obj.nanosecondsOfSecond) sink.writeInt(obj.nanosecondsOfSecond)
} }
override fun readObject(input: Input): Instant { override fun readFrom(source: Source): Instant {
val seconds = input.readLong() val seconds = source.readLong()
val nanoseconds = input.readInt() val nanoseconds = source.readInt()
return Instant.fromEpochSeconds(seconds, nanoseconds) return Instant.fromEpochSeconds(seconds, nanoseconds)
} }
} }

@ -6,7 +6,8 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.flow.receiveAsFlow
import space.kscience.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 space.kscience.dataforge.misc.DfType
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
/** /**
@ -17,7 +18,7 @@ public interface Port : ContextAware, Socket<ByteArray>
/** /**
* A specialized factory for [Port] * A specialized factory for [Port]
*/ */
@Type(PortFactory.TYPE) @DfType(PortFactory.TYPE)
public interface PortFactory : Factory<Port> { public interface PortFactory : Factory<Port> {
public val type: String public val type: String
@ -37,7 +38,7 @@ public abstract class AbstractPort(
protected val scope: CoroutineScope = CoroutineScope(coroutineContext + SupervisorJob(coroutineContext[Job])) protected val scope: CoroutineScope = CoroutineScope(coroutineContext + SupervisorJob(coroutineContext[Job]))
private val outgoing = Channel<ByteArray>(100) private val outgoing = Channel<ByteArray>(100)
private val incoming = Channel<ByteArray>(Channel.CONFLATED) private val incoming = Channel<ByteArray>(100)
init { init {
scope.coroutineContext[Job]?.invokeOnCompletion { scope.coroutineContext[Job]?.invokeOnCompletion {
@ -87,7 +88,6 @@ public abstract class AbstractPort(
override fun close() { override fun close() {
outgoing.close() outgoing.close()
incoming.close() incoming.close()
sendJob.cancel()
scope.cancel() scope.cancel()
} }

@ -0,0 +1,5 @@
package space.kscience.controls.ports
import space.kscience.dataforge.io.Binary
public fun Binary.readShort(position: Int): Short = read(position) { readShort() }

@ -1,12 +1,11 @@
package space.kscience.controls.ports package space.kscience.controls.ports
import io.ktor.utils.io.core.BytePacketBuilder
import io.ktor.utils.io.core.readBytes
import io.ktor.utils.io.core.reset
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onCompletion import kotlinx.coroutines.flow.onCompletion
import kotlinx.coroutines.flow.transform import kotlinx.coroutines.flow.transform
import kotlinx.io.Buffer
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.
@ -14,7 +13,7 @@ import kotlinx.coroutines.flow.transform
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" }
val output = BytePacketBuilder() val output = Buffer()
var matcherPosition = 0 var matcherPosition = 0
onCompletion { onCompletion {
@ -29,9 +28,8 @@ public fun Flow<ByteArray>.withDelimiter(delimiter: ByteArray): Flow<ByteArray>
matcherPosition++ matcherPosition++
if (matcherPosition == delimiter.size) { if (matcherPosition == delimiter.size) {
//full match achieved, sending result //full match achieved, sending result
val bytes = output.build() emit(output.readByteArray())
emit(bytes.readBytes()) output.clear()
output.reset()
matcherPosition = 0 matcherPosition = 0
} }
} else if (matcherPosition > 0) { } else if (matcherPosition > 0) {

@ -9,9 +9,14 @@ import space.kscience.dataforge.meta.transformations.MetaConverter
import kotlin.properties.PropertyDelegateProvider import kotlin.properties.PropertyDelegateProvider
import kotlin.properties.ReadOnlyProperty import kotlin.properties.ReadOnlyProperty
import kotlin.reflect.KProperty import kotlin.reflect.KProperty
import kotlin.reflect.KType
import kotlin.reflect.typeOf
public object UnitMetaConverter : MetaConverter<Unit> { public object UnitMetaConverter : MetaConverter<Unit> {
override fun metaToObject(meta: Meta): Unit = Unit
override val type: KType = typeOf<Unit>()
override fun metaToObjectOrNull(meta: Meta): Unit = Unit
override fun objectToMeta(obj: Unit): Meta = Meta.EMPTY override fun objectToMeta(obj: Unit): Meta = Meta.EMPTY
} }

@ -2,6 +2,8 @@ package space.kscience.controls.spec
import space.kscience.dataforge.meta.* import space.kscience.dataforge.meta.*
import space.kscience.dataforge.meta.transformations.MetaConverter import space.kscience.dataforge.meta.transformations.MetaConverter
import kotlin.reflect.KType
import kotlin.reflect.typeOf
import kotlin.time.Duration import kotlin.time.Duration
import kotlin.time.DurationUnit import kotlin.time.DurationUnit
import kotlin.time.toDuration import kotlin.time.toDuration
@ -10,7 +12,9 @@ public fun Double.asMeta(): Meta = Meta(asValue())
//TODO to be moved to DF //TODO to be moved to DF
public object DurationConverter : MetaConverter<Duration> { public object DurationConverter : MetaConverter<Duration> {
override fun metaToObject(meta: Meta): Duration = meta.value?.double?.toDuration(DurationUnit.SECONDS) override val type: KType = typeOf<Duration>()
override fun metaToObjectOrNull(meta: Meta): Duration = meta.value?.double?.toDuration(DurationUnit.SECONDS)
?: run { ?: run {
val unit: DurationUnit = meta["unit"].enum<DurationUnit>() ?: DurationUnit.SECONDS val unit: DurationUnit = meta["unit"].enum<DurationUnit>() ?: DurationUnit.SECONDS
val value = meta[Meta.VALUE_KEY].double ?: error("No value present for Duration") val value = meta[Meta.VALUE_KEY].double ?: error("No value present for Duration")

@ -68,7 +68,7 @@ public fun <D : Device> DeviceSpec<D>.booleanProperty(
MetaConverter.boolean, MetaConverter.boolean,
{ {
metaDescriptor { metaDescriptor {
type(ValueType.BOOLEAN) valueType(ValueType.BOOLEAN)
} }
descriptorBuilder() descriptorBuilder()
}, },
@ -80,7 +80,7 @@ private inline fun numberDescriptor(
crossinline descriptorBuilder: PropertyDescriptor.() -> Unit = {} crossinline descriptorBuilder: PropertyDescriptor.() -> Unit = {}
): PropertyDescriptor.() -> Unit = { ): PropertyDescriptor.() -> Unit = {
metaDescriptor { metaDescriptor {
type(ValueType.NUMBER) valueType(ValueType.NUMBER)
} }
descriptorBuilder() descriptorBuilder()
} }
@ -115,7 +115,7 @@ public fun <D : Device> DeviceSpec<D>.stringProperty(
MetaConverter.string, MetaConverter.string,
{ {
metaDescriptor { metaDescriptor {
type(ValueType.STRING) valueType(ValueType.STRING)
} }
descriptorBuilder() descriptorBuilder()
}, },
@ -131,7 +131,7 @@ public fun <D : Device> DeviceSpec<D>.metaProperty(
MetaConverter.meta, MetaConverter.meta,
{ {
metaDescriptor { metaDescriptor {
type(ValueType.STRING) valueType(ValueType.STRING)
} }
descriptorBuilder() descriptorBuilder()
}, },
@ -151,7 +151,7 @@ public fun <D : Device> DeviceSpec<D>.booleanProperty(
MetaConverter.boolean, MetaConverter.boolean,
{ {
metaDescriptor { metaDescriptor {
type(ValueType.BOOLEAN) valueType(ValueType.BOOLEAN)
} }
descriptorBuilder() descriptorBuilder()
}, },

@ -8,6 +8,7 @@ import space.kscience.dataforge.context.logger
import space.kscience.dataforge.meta.* import space.kscience.dataforge.meta.*
import java.net.InetSocketAddress import java.net.InetSocketAddress
import java.nio.ByteBuffer import java.nio.ByteBuffer
import java.nio.channels.AsynchronousCloseException
import java.nio.channels.ByteChannel import java.nio.channels.ByteChannel
import java.nio.channels.DatagramChannel import java.nio.channels.DatagramChannel
import java.nio.channels.SocketChannel import java.nio.channels.SocketChannel
@ -30,7 +31,7 @@ public class ChannelPort(
channelBuilder: suspend () -> ByteChannel, channelBuilder: suspend () -> ByteChannel,
) : AbstractPort(context, coroutineContext), AutoCloseable { ) : AbstractPort(context, coroutineContext), AutoCloseable {
private val futureChannel: Deferred<ByteChannel> = this.scope.async(Dispatchers.IO) { private val futureChannel: Deferred<ByteChannel> = scope.async(Dispatchers.IO) {
channelBuilder() channelBuilder()
} }
@ -39,10 +40,10 @@ public class ChannelPort(
*/ */
public val startJob: Job get() = futureChannel public val startJob: Job get() = futureChannel
private val listenerJob = this.scope.launch(Dispatchers.IO) { private val listenerJob = scope.launch(Dispatchers.IO) {
val channel = futureChannel.await() val channel = futureChannel.await()
val buffer = ByteBuffer.allocate(1024) val buffer = ByteBuffer.allocate(1024)
while (isActive) { while (isActive && channel.isOpen) {
try { try {
val num = channel.read(buffer) val num = channel.read(buffer)
if (num > 0) { if (num > 0) {
@ -50,8 +51,12 @@ public class ChannelPort(
} }
if (num < 0) cancel("The input channel is exhausted") if (num < 0) cancel("The input channel is exhausted")
} catch (ex: Exception) { } catch (ex: Exception) {
logger.error(ex) { "Channel read error" } if (ex is AsynchronousCloseException) {
delay(1000) logger.info { "Channel $channel closed" }
} else {
logger.error(ex) { "Channel read error, retrying in 1 second" }
delay(1000)
}
} }
} }
} }
@ -62,11 +67,8 @@ public class ChannelPort(
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
override fun close() { override fun close() {
listenerJob.cancel()
if (futureChannel.isCompleted) { if (futureChannel.isCompleted) {
futureChannel.getCompleted().close() futureChannel.getCompleted().close()
} else {
futureChannel.cancel()
} }
super.close() super.close()
} }
@ -106,7 +108,7 @@ public object UdpPort : PortFactory {
/** /**
* Connect a datagram channel to a remote host/port. If [localPort] is provided, it is used to bind local port for receiving messages. * Connect a datagram channel to a remote host/port. If [localPort] is provided, it is used to bind local port for receiving messages.
*/ */
public fun open( public fun openChannel(
context: Context, context: Context,
remoteHost: String, remoteHost: String,
remotePort: Int, remotePort: Int,
@ -128,6 +130,6 @@ public object UdpPort : PortFactory {
val remotePort by meta.number { error("Remote port is not specified") } val remotePort by meta.number { error("Remote port is not specified") }
val localHost: String? by meta.string() val localHost: String? by meta.string()
val localPort: Int? by meta.int() val localPort: Int? by meta.int()
return open(context, remoteHost, remotePort.toInt(), localPort, localHost ?: "localhost") return openChannel(context, remoteHost, remotePort.toInt(), localPort, localHost ?: "localhost")
} }
} }

@ -1,25 +1,50 @@
package space.kscience.controls.ports package space.kscience.controls.ports
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.take
import kotlinx.coroutines.flow.toList import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import space.kscience.dataforge.context.Global
import kotlin.test.assertEquals import kotlin.test.assertEquals
internal class PortIOTest{ internal class PortIOTest {
@Test @Test
fun testDelimiteredByteArrayFlow(){ fun testDelimiteredByteArrayFlow() {
val flow = flowOf("bb?b","ddd?",":defgb?:ddf","34fb?:--").map { it.encodeToByteArray() } val flow = flowOf("bb?b", "ddd?", ":defgb?:ddf", "34fb?:--").map { it.encodeToByteArray() }
val chunked = flow.withDelimiter("?:".encodeToByteArray()) val chunked = flow.withDelimiter("?:".encodeToByteArray())
runBlocking { runBlocking {
val result = chunked.toList() val result = chunked.toList()
assertEquals(3, result.size) assertEquals(3, result.size)
assertEquals("bb?bddd?:",result[0].decodeToString()) assertEquals("bb?bddd?:", result[0].decodeToString())
assertEquals("defgb?:", result[1].decodeToString()) assertEquals("defgb?:", result[1].decodeToString())
assertEquals("ddf34fb?:", result[2].decodeToString()) assertEquals("ddf34fb?:", result[2].decodeToString())
} }
} }
@Test
fun testUdpCommunication() = runTest {
val receiver = UdpPort.openChannel(Global, "localhost", 8811, localPort = 8812)
val sender = UdpPort.openChannel(Global, "localhost", 8812, localPort = 8811)
delay(30)
repeat(10) {
sender.send("Line number $it\n")
}
val res = receiver
.receiving()
.withStringDelimiter("\n")
.take(10)
.toList()
assertEquals("Line number 3", res[3].trim())
receiver.close()
sender.close()
}
} }

@ -1,12 +1,14 @@
package space.kscience.controls.modbus package space.kscience.controls.modbus
import com.ghgande.j2mod.modbus.procimg.* import com.ghgande.j2mod.modbus.procimg.*
import io.ktor.utils.io.core.*
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.io.Buffer
import space.kscience.controls.api.Device import space.kscience.controls.api.Device
import space.kscience.controls.ports.readShort
import space.kscience.controls.spec.* import space.kscience.controls.spec.*
import space.kscience.dataforge.io.Binary
public class DeviceProcessImageBuilder<D : Device> internal constructor( public class DeviceProcessImageBuilder<D : Device> internal constructor(
@ -106,11 +108,11 @@ public class DeviceProcessImageBuilder<D : Device> internal constructor(
} }
device.useProperty(propertySpec) { value -> device.useProperty(propertySpec) { value ->
val packet = buildPacket { val binary = Binary {
key.format.writeObject(this, value) key.format.writeTo(this, value)
}.readByteBuffer() }
registers.forEachIndexed { index, register -> registers.forEachIndexed { index, register ->
register.setValue(packet.getShort(index * 2)) register.setValue(binary.readShort(index * 2))
} }
} }
} }
@ -118,7 +120,7 @@ public class DeviceProcessImageBuilder<D : Device> internal constructor(
/** /**
* Trigger [block] if one of register changes. * Trigger [block] if one of register changes.
*/ */
private fun List<ObservableRegister>.onChange(block: suspend (ByteReadPacket) -> Unit) { private fun List<ObservableRegister>.onChange(block: suspend (Buffer) -> Unit) {
var ready = false var ready = false
forEach { register -> forEach { register ->
@ -128,7 +130,7 @@ public class DeviceProcessImageBuilder<D : Device> internal constructor(
} }
device.launch { device.launch {
val builder = BytePacketBuilder() val builder = Buffer()
while (isActive) { while (isActive) {
delay(1) delay(1)
if (ready) { if (ready) {
@ -136,7 +138,7 @@ public class DeviceProcessImageBuilder<D : Device> internal constructor(
forEach { value -> forEach { value ->
writeShort(value.toShort()) writeShort(value.toShort())
} }
}.build() }
block(packet) block(packet)
ready = false ready = false
} }
@ -154,15 +156,15 @@ public class DeviceProcessImageBuilder<D : Device> internal constructor(
} }
registers.onChange { packet -> registers.onChange { packet ->
device.write(propertySpec, key.format.readObject(packet)) device.write(propertySpec, key.format.readFrom(packet))
} }
device.useProperty(propertySpec) { value -> device.useProperty(propertySpec) { value ->
val packet = buildPacket { val binary = Binary {
key.format.writeObject(this, value) key.format.writeTo(this, value)
}.readByteBuffer() }
registers.forEachIndexed { index, observableRegister -> registers.forEachIndexed { index, observableRegister ->
observableRegister.setValue(packet.getShort(index * 2)) observableRegister.setValue(binary.readShort(index * 2))
} }
} }
} }
@ -212,7 +214,7 @@ public class DeviceProcessImageBuilder<D : Device> internal constructor(
registers.onChange { packet -> registers.onChange { packet ->
device.launch { device.launch {
device.action(key.format.readObject(packet)) device.action(key.format.readFrom(packet))
} }
} }

@ -5,11 +5,10 @@ import com.ghgande.j2mod.modbus.procimg.InputRegister
import com.ghgande.j2mod.modbus.procimg.Register import com.ghgande.j2mod.modbus.procimg.Register
import com.ghgande.j2mod.modbus.procimg.SimpleInputRegister import com.ghgande.j2mod.modbus.procimg.SimpleInputRegister
import com.ghgande.j2mod.modbus.util.BitVector import com.ghgande.j2mod.modbus.util.BitVector
import io.ktor.utils.io.core.ByteReadPacket import kotlinx.io.Buffer
import io.ktor.utils.io.core.buildPacket
import io.ktor.utils.io.core.readByteBuffer
import io.ktor.utils.io.core.writeShort
import space.kscience.controls.api.Device import space.kscience.controls.api.Device
import space.kscience.dataforge.io.Buffer
import space.kscience.dataforge.io.ByteArray
import java.nio.ByteBuffer import java.nio.ByteBuffer
import kotlin.properties.ReadWriteProperty import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty import kotlin.reflect.KProperty
@ -45,7 +44,7 @@ public interface ModbusDevice : Device {
public operator fun <T> ModbusRegistryKey.InputRange<T>.getValue(thisRef: Any?, property: KProperty<*>): T { public operator fun <T> ModbusRegistryKey.InputRange<T>.getValue(thisRef: Any?, property: KProperty<*>): T {
val packet = readInputRegistersToPacket(address, count) val packet = readInputRegistersToPacket(address, count)
return format.readObject(packet) return format.readFrom(packet)
} }
@ -62,7 +61,7 @@ public interface ModbusDevice : Device {
public operator fun <T> ModbusRegistryKey.HoldingRange<T>.getValue(thisRef: Any?, property: KProperty<*>): T { public operator fun <T> ModbusRegistryKey.HoldingRange<T>.getValue(thisRef: Any?, property: KProperty<*>): T {
val packet = readHoldingRegistersToPacket(address, count) val packet = readHoldingRegistersToPacket(address, count)
return format.readObject(packet) return format.readFrom(packet)
} }
public operator fun <T> ModbusRegistryKey.HoldingRange<T>.setValue( public operator fun <T> ModbusRegistryKey.HoldingRange<T>.setValue(
@ -70,9 +69,9 @@ public interface ModbusDevice : Device {
property: KProperty<*>, property: KProperty<*>,
value: T, value: T,
) { ) {
val buffer = buildPacket { val buffer = ByteArray {
format.writeObject(this, value) format.writeTo(this, value)
}.readByteBuffer() }
writeHoldingRegisters(address, buffer) writeHoldingRegisters(address, buffer)
} }
@ -122,7 +121,7 @@ private fun Array<out InputRegister>.toBuffer(): ByteBuffer {
return buffer return buffer
} }
private fun Array<out InputRegister>.toPacket(): ByteReadPacket = buildPacket { private fun Array<out InputRegister>.toPacket(): Buffer = Buffer {
forEach { value -> forEach { value ->
writeShort(value.toShort()) writeShort(value.toShort())
} }
@ -131,7 +130,7 @@ private fun Array<out InputRegister>.toPacket(): ByteReadPacket = buildPacket {
public fun ModbusDevice.readInputRegistersToBuffer(address: Int, count: Int): ByteBuffer = public fun ModbusDevice.readInputRegistersToBuffer(address: Int, count: Int): ByteBuffer =
master.readInputRegisters(unitId, address, count).toBuffer() master.readInputRegisters(unitId, address, count).toBuffer()
public fun ModbusDevice.readInputRegistersToPacket(address: Int, count: Int): ByteReadPacket = public fun ModbusDevice.readInputRegistersToPacket(address: Int, count: Int): Buffer =
master.readInputRegisters(unitId, address, count).toPacket() master.readInputRegisters(unitId, address, count).toPacket()
public fun ModbusDevice.readDoubleInput(address: Int): Double = public fun ModbusDevice.readDoubleInput(address: Int): Double =
@ -151,7 +150,7 @@ public fun ModbusDevice.readHoldingRegisters(address: Int, count: Int): List<Reg
public fun ModbusDevice.readHoldingRegistersToBuffer(address: Int, count: Int): ByteBuffer = public fun ModbusDevice.readHoldingRegistersToBuffer(address: Int, count: Int): ByteBuffer =
master.readMultipleRegisters(unitId, address, count).toBuffer() master.readMultipleRegisters(unitId, address, count).toBuffer()
public fun ModbusDevice.readHoldingRegistersToPacket(address: Int, count: Int): ByteReadPacket = public fun ModbusDevice.readHoldingRegistersToPacket(address: Int, count: Int): Buffer =
master.readMultipleRegisters(unitId, address, count).toPacket() master.readMultipleRegisters(unitId, address, count).toPacket()
public fun ModbusDevice.readDoubleRegister(address: Int): Double = public fun ModbusDevice.readDoubleRegister(address: Int): Double =
@ -183,6 +182,13 @@ public fun ModbusDevice.writeHoldingRegisters(address: Int, buffer: ByteBuffer):
return writeHoldingRegisters(address, array) return writeHoldingRegisters(address, array)
} }
public fun ModbusDevice.writeHoldingRegisters(address: Int, byteArray: ByteArray): Int {
val buffer = ByteBuffer.wrap(byteArray)
val array: ShortArray = ShortArray(buffer.limit().floorDiv(2)) { buffer.getShort(it * 2) }
return writeHoldingRegisters(address, array)
}
public fun ModbusDevice.modbusRegister( public fun ModbusDevice.modbusRegister(
address: Int, address: Int,
): ReadWriteProperty<ModbusDevice, Short> = object : ReadWriteProperty<ModbusDevice, Short> { ): ReadWriteProperty<ModbusDevice, Short> = object : ReadWriteProperty<ModbusDevice, Short> {

@ -107,7 +107,7 @@ internal class MetaStructureCodec(
override fun createStructure(name: String, members: LinkedHashMap<String, Meta>): Meta = Meta { override fun createStructure(name: String, members: LinkedHashMap<String, Meta>): Meta = Meta {
members.forEach { (property: String, value: Meta?) -> members.forEach { (property: String, value: Meta?) ->
setMeta(Name.parse(property), value) set(Name.parse(property), value)
} }
} }

@ -43,7 +43,7 @@ public suspend inline fun <reified T: Any> OpcUaDevice.readOpcWithTime(
else -> error("Incompatible OPC property value $content") else -> error("Incompatible OPC property value $content")
} }
val res: T = converter.metaToObject(meta) ?: error("Meta $meta could not be converted to ${T::class}") val res: T = converter.metaToObject(meta)
return res to time return res to time
} }

@ -9,6 +9,7 @@ import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.names.Name import space.kscience.dataforge.names.Name
import space.kscience.visionforge.ElementVisionRenderer import space.kscience.visionforge.ElementVisionRenderer
import space.kscience.visionforge.Vision import space.kscience.visionforge.Vision
import space.kscience.visionforge.VisionClient
import space.kscience.visionforge.VisionPlugin import space.kscience.visionforge.VisionPlugin
public actual class ControlVisionPlugin : VisionPlugin(), ElementVisionRenderer { public actual class ControlVisionPlugin : VisionPlugin(), ElementVisionRenderer {
@ -20,7 +21,7 @@ public actual class ControlVisionPlugin : VisionPlugin(), ElementVisionRenderer
TODO("Not yet implemented") TODO("Not yet implemented")
} }
override fun render(element: Element, name: Name, vision: Vision, meta: Meta) { override fun render(element: Element, client: VisionClient, name: Name, vision: Vision, meta: Meta) {
TODO("Not yet implemented") TODO("Not yet implemented")
} }

@ -42,7 +42,7 @@ class DemoDevice(context: Context, meta: Meta) : DeviceBySpec<IDemoDevice>(Compa
// register virtual properties based on actual object state // register virtual properties based on actual object state
val timeScale by mutableProperty(MetaConverter.double, IDemoDevice::timeScaleState) { val timeScale by mutableProperty(MetaConverter.double, IDemoDevice::timeScaleState) {
metaDescriptor { metaDescriptor {
type(ValueType.NUMBER) valueType(ValueType.NUMBER)
} }
description = "Real to virtual time scale" description = "Real to virtual time scale"
} }

@ -17,6 +17,8 @@ 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
import kotlin.math.pow import kotlin.math.pow
import kotlin.reflect.KType
import kotlin.reflect.typeOf
import kotlin.time.Duration import kotlin.time.Duration
import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.ExperimentalTime import kotlin.time.ExperimentalTime
@ -28,7 +30,10 @@ data class Vector2D(var x: Double = 0.0, var y: Double = 0.0) : MetaRepr {
operator fun div(arg: Double): Vector2D = Vector2D(x / arg, y / arg) operator fun div(arg: Double): Vector2D = Vector2D(x / arg, y / arg)
companion object CoordinatesMetaConverter : MetaConverter<Vector2D> { companion object CoordinatesMetaConverter : MetaConverter<Vector2D> {
override fun metaToObject(meta: Meta): Vector2D = Vector2D(
override val type: KType = typeOf<Vector2D>()
override fun metaToObjectOrNull(meta: Meta): Vector2D = Vector2D(
meta["x"].double ?: 0.0, meta["x"].double ?: 0.0,
meta["y"].double ?: 0.0 meta["y"].double ?: 0.0
) )
@ -40,7 +45,8 @@ data class Vector2D(var x: Double = 0.0, var y: Double = 0.0) : MetaRepr {
} }
} }
open class VirtualCar(context: Context, meta: Meta) : DeviceBySpec<VirtualCar>(IVirtualCar, context, meta), IVirtualCar { open class VirtualCar(context: Context, meta: Meta) : DeviceBySpec<VirtualCar>(IVirtualCar, context, meta),
IVirtualCar {
private val clock = context.clock private val clock = context.clock
private val timeScale = 1e-3 private val timeScale = 1e-3

@ -3,7 +3,7 @@ import org.jetbrains.kotlin.gradle.dsl.ExplicitApiMode
plugins { plugins {
id("space.kscience.gradle.mpp") id("space.kscience.gradle.mpp")
id("org.jetbrains.compose") version "1.5.10" id("org.jetbrains.compose") version "1.5.11"
} }
kscience { kscience {

@ -1,10 +0,0 @@
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 {}
}

@ -7,4 +7,4 @@ org.gradle.parallel=true
org.gradle.configureondemand=true org.gradle.configureondemand=true
org.gradle.jvmargs=-Xmx4096m org.gradle.jvmargs=-Xmx4096m
toolsVersion=0.15.0-kotlin-1.9.20 toolsVersion=0.15.2-kotlin-1.9.21

4
magix/README.md Normal file

@ -0,0 +1,4 @@
# Module magix

@ -17,6 +17,10 @@ kscience {
useSerialization{ useSerialization{
json() json()
} }
commonMain{
implementation(spclibs.atomicfu)
}
} }
readme{ readme{