From 78dade4b4969f64c4316e36fc02e84e9969614af Mon Sep 17 00:00:00 2001 From: Alexander Nozik Date: Mon, 18 Mar 2024 17:18:31 +0300 Subject: [PATCH] PLC4x bindings --- controls-plc4x/build.gradle.kts | 24 ++++ .../kscience/controls/plc4x/Plc4XDevice.kt | 28 ++++ .../kscience/controls/plc4x/Plc4xProperty.kt | 31 +++++ .../kscience/controls/plc4x/plc4xConnector.kt | 123 ++++++++++++++++++ 4 files changed, 206 insertions(+) create mode 100644 controls-plc4x/build.gradle.kts create mode 100644 controls-plc4x/src/jvmMain/kotlin/space/kscience/controls/plc4x/Plc4XDevice.kt create mode 100644 controls-plc4x/src/jvmMain/kotlin/space/kscience/controls/plc4x/Plc4xProperty.kt create mode 100644 controls-plc4x/src/jvmMain/kotlin/space/kscience/controls/plc4x/plc4xConnector.kt diff --git a/controls-plc4x/build.gradle.kts b/controls-plc4x/build.gradle.kts new file mode 100644 index 0000000..0836476 --- /dev/null +++ b/controls-plc4x/build.gradle.kts @@ -0,0 +1,24 @@ +import space.kscience.gradle.Maturity + +plugins { + id("space.kscience.gradle.mpp") + `maven-publish` +} + +val plc4xVersion = "0.12.0" + +description = """ + A plugin for Controls-kt device server on top of plc4x library +""".trimIndent() + +kscience{ + jvm() + jvmMain{ + api(projects.controlsCore) + api("org.apache.plc4x:plc4j-spi:$plc4xVersion") + } +} + +readme{ + maturity = Maturity.EXPERIMENTAL +} \ No newline at end of file diff --git a/controls-plc4x/src/jvmMain/kotlin/space/kscience/controls/plc4x/Plc4XDevice.kt b/controls-plc4x/src/jvmMain/kotlin/space/kscience/controls/plc4x/Plc4XDevice.kt new file mode 100644 index 0000000..996d194 --- /dev/null +++ b/controls-plc4x/src/jvmMain/kotlin/space/kscience/controls/plc4x/Plc4XDevice.kt @@ -0,0 +1,28 @@ +package space.kscience.controls.plc4x + +import kotlinx.coroutines.future.await +import org.apache.plc4x.java.api.PlcConnection +import org.apache.plc4x.java.api.messages.PlcWriteRequest +import space.kscience.controls.api.Device +import space.kscience.dataforge.meta.Meta + +public interface Plc4XDevice: Device { + public val connection: PlcConnection + + public suspend fun read(plc4xProperty: Plc4xProperty): Meta = with(plc4xProperty){ + val request = connection.readRequestBuilder().request().build() + val response = request.execute().await() + response.readProperty() + } + + public suspend fun write(plc4xProperty: Plc4xProperty, value: Meta): Unit = with(plc4xProperty){ + val request: PlcWriteRequest = connection.writeRequestBuilder().writeProperty(value).build() + request.execute().await() + } + + public suspend fun subscribe(propertyName: String, plc4xProperty: Plc4xProperty): Unit = with(plc4xProperty){ + + } + +} + diff --git a/controls-plc4x/src/jvmMain/kotlin/space/kscience/controls/plc4x/Plc4xProperty.kt b/controls-plc4x/src/jvmMain/kotlin/space/kscience/controls/plc4x/Plc4xProperty.kt new file mode 100644 index 0000000..5b16dec --- /dev/null +++ b/controls-plc4x/src/jvmMain/kotlin/space/kscience/controls/plc4x/Plc4xProperty.kt @@ -0,0 +1,31 @@ +package space.kscience.controls.plc4x + +import org.apache.plc4x.java.api.messages.PlcReadRequest +import org.apache.plc4x.java.api.messages.PlcReadResponse +import org.apache.plc4x.java.api.messages.PlcWriteRequest +import org.apache.plc4x.java.api.types.PlcValueType +import space.kscience.dataforge.meta.Meta + +public interface Plc4xProperty { + + public fun PlcReadRequest.Builder.request(): PlcReadRequest.Builder + + public fun PlcReadResponse.readProperty(): Meta + + public fun PlcWriteRequest.Builder.writeProperty(meta: Meta): PlcWriteRequest.Builder +} + +public class DefaultPlc4xProperty( + private val address: String, + private val plcValueType: PlcValueType, + private val name: String = "@default", +) : Plc4xProperty { + + override fun PlcReadRequest.Builder.request(): PlcReadRequest.Builder = + addTagAddress(name, address) + override fun PlcReadResponse.readProperty(): Meta = + asPlcValue.toMeta() + + override fun PlcWriteRequest.Builder.writeProperty(meta: Meta): PlcWriteRequest.Builder = + addTagAddress(name, address, meta.toPlcValue(plcValueType)) +} \ No newline at end of file diff --git a/controls-plc4x/src/jvmMain/kotlin/space/kscience/controls/plc4x/plc4xConnector.kt b/controls-plc4x/src/jvmMain/kotlin/space/kscience/controls/plc4x/plc4xConnector.kt new file mode 100644 index 0000000..5d0b2de --- /dev/null +++ b/controls-plc4x/src/jvmMain/kotlin/space/kscience/controls/plc4x/plc4xConnector.kt @@ -0,0 +1,123 @@ +package space.kscience.controls.plc4x + +import org.apache.plc4x.java.api.types.PlcValueType +import org.apache.plc4x.java.api.value.PlcValue +import org.apache.plc4x.java.spi.values.* +import space.kscience.dataforge.meta.* +import space.kscience.dataforge.names.asName +import java.math.BigInteger + +internal fun PlcValue.toMeta(): Meta = Meta { + when (plcValueType) { + null, PlcValueType.NULL -> value = Null + PlcValueType.BOOL -> value = this@toMeta.boolean.asValue() + PlcValueType.BYTE -> this@toMeta.byte.asValue() + PlcValueType.WORD -> this@toMeta.short.asValue() + PlcValueType.DWORD -> this@toMeta.int.asValue() + PlcValueType.LWORD -> this@toMeta.long.asValue() + PlcValueType.USINT -> this@toMeta.short.asValue() + PlcValueType.UINT -> this@toMeta.int.asValue() + PlcValueType.UDINT -> this@toMeta.long.asValue() + PlcValueType.ULINT -> this@toMeta.bigInteger.asValue() + PlcValueType.SINT -> this@toMeta.byte.asValue() + PlcValueType.INT -> this@toMeta.short.asValue() + PlcValueType.DINT -> this@toMeta.int.asValue() + PlcValueType.LINT -> this@toMeta.long.asValue() + PlcValueType.REAL -> this@toMeta.float.asValue() + PlcValueType.LREAL -> this@toMeta.double.asValue() + PlcValueType.CHAR -> this@toMeta.int.asValue() + PlcValueType.WCHAR -> this@toMeta.short.asValue() + PlcValueType.STRING -> this@toMeta.string.asValue() + PlcValueType.WSTRING -> this@toMeta.string.asValue() + PlcValueType.TIME -> this@toMeta.duration.toString().asValue() + PlcValueType.LTIME -> this@toMeta.duration.toString().asValue() + PlcValueType.DATE -> this@toMeta.date.toString().asValue() + PlcValueType.LDATE -> this@toMeta.date.toString().asValue() + PlcValueType.TIME_OF_DAY -> this@toMeta.time.toString().asValue() + PlcValueType.LTIME_OF_DAY -> this@toMeta.time.toString().asValue() + PlcValueType.DATE_AND_TIME -> this@toMeta.dateTime.toString().asValue() + PlcValueType.DATE_AND_LTIME -> this@toMeta.dateTime.toString().asValue() + PlcValueType.LDATE_AND_TIME -> this@toMeta.dateTime.toString().asValue() + PlcValueType.Struct -> this@toMeta.struct.forEach { (name, item) -> + set(name, item.toMeta()) + } + + PlcValueType.List -> { + val listOfMeta = this@toMeta.list.map { it.toMeta() } + if (listOfMeta.all { it.items.isEmpty() }) { + value = listOfMeta.map { it.value ?: Null }.asValue() + } else { + setIndexed("@list".asName(), list.map { it.toMeta() }) + } + } + + PlcValueType.RAW_BYTE_ARRAY -> this@toMeta.raw.asValue() + } +} + +private fun Value.toPlcValue(): PlcValue = when (type) { + ValueType.NUMBER -> when (val number = number) { + is Short -> PlcINT(number.toShort()) + is Int -> PlcDINT(number.toInt()) + is Long -> PlcLINT(number.toLong()) + is Float -> PlcREAL(number.toFloat()) + else -> PlcLREAL(number.toDouble()) + } + + ValueType.STRING -> PlcSTRING(string) + ValueType.BOOLEAN -> PlcBOOL(boolean) + ValueType.NULL -> PlcNull() + ValueType.LIST -> TODO() +} + +internal fun Meta.toPlcValue(hint: PlcValueType): PlcValue = when (hint) { + PlcValueType.Struct -> PlcStruct( + items.entries.associate { (token, item) -> + token.toString() to item.toPlcValue(PlcValueType.Struct) + } + ) + + PlcValueType.NULL -> PlcNull() + PlcValueType.BOOL -> PlcBOOL(boolean) + PlcValueType.BYTE -> PlcBYTE(int) + PlcValueType.WORD -> PlcWORD(int) + PlcValueType.DWORD -> PlcDWORD(int) + PlcValueType.LWORD -> PlcLWORD(long) + PlcValueType.USINT -> PlcLWORD(short) + PlcValueType.UINT -> PlcUINT(int) + PlcValueType.UDINT -> PlcDINT(long) + PlcValueType.ULINT -> (number as? BigInteger)?.let { PlcULINT(it) } ?: PlcULINT(long) + PlcValueType.SINT -> PlcSINT(int) + PlcValueType.INT -> PlcINT(int) + PlcValueType.DINT -> PlcDINT(int) + PlcValueType.LINT -> PlcLINT(long) + PlcValueType.REAL -> PlcREAL(float) + PlcValueType.LREAL -> PlcLREAL(double) + PlcValueType.CHAR -> PlcCHAR(int) + PlcValueType.WCHAR -> PlcWCHAR(short) + PlcValueType.STRING -> PlcSTRING(string) + PlcValueType.WSTRING -> PlcWSTRING(string) + PlcValueType.TIME -> PlcTIME(string?.let { java.time.Duration.parse(it) }) + PlcValueType.LTIME -> PlcLTIME(string?.let { java.time.Duration.parse(it) }) + PlcValueType.DATE -> PlcDATE(string?.let { java.time.LocalDate.parse(it) }) + PlcValueType.LDATE -> PlcLDATE(string?.let { java.time.LocalDate.parse(it) }) + PlcValueType.TIME_OF_DAY -> PlcTIME_OF_DAY(string?.let { java.time.LocalTime.parse(it) }) + PlcValueType.LTIME_OF_DAY -> PlcLTIME_OF_DAY(string?.let { java.time.LocalTime.parse(it) }) + PlcValueType.DATE_AND_TIME -> PlcDATE_AND_TIME(string?.let { java.time.LocalDateTime.parse(it) }) + PlcValueType.DATE_AND_LTIME -> PlcDATE_AND_LTIME(string?.let { java.time.LocalDateTime.parse(it) }) + PlcValueType.LDATE_AND_TIME -> PlcLDATE_AND_TIME(string?.let { java.time.LocalDateTime.parse(it) }) + PlcValueType.List -> PlcList().apply { + value?.list?.forEach { add(it.toPlcValue()) } + getIndexed("@list").forEach { (_, meta) -> + if (meta.items.isEmpty()) { + meta.value?.let { add(it.toPlcValue()) } + } else { + add(meta.toPlcValue(PlcValueType.Struct)) + } + } + } + + PlcValueType.RAW_BYTE_ARRAY -> PlcRawByteArray( + value?.list?.map { it.number.toByte() }?.toByteArray() ?: error("The meta content is not byte array") + ) +} \ No newline at end of file