From 20951a0b0f47609b30ecf90ffca650f76639bbe8 Mon Sep 17 00:00:00 2001 From: Alexander Nozik Date: Sat, 10 Jun 2023 16:17:54 +0300 Subject: [PATCH] Modbus revision acording to modern standards --- .../space/kscience/controls/api/Device.kt | 17 +- .../kscience/controls/spec/DeviceBase.kt | 36 +- .../kscience/controls/spec/DeviceBySpec.kt | 5 +- .../controls/modbus/DeviceProcessImage.kt | 321 +++++++++++------- .../kscience/controls/modbus/ModbusDevice.kt | 105 +++--- .../controls/modbus/ModbusDeviceBySpec.kt | 15 +- .../controls/modbus/ModbusRegistryMap.kt | 147 ++++---- 7 files changed, 386 insertions(+), 260 deletions(-) diff --git a/controls-core/src/commonMain/kotlin/space/kscience/controls/api/Device.kt b/controls-core/src/commonMain/kotlin/space/kscience/controls/api/Device.kt index 5056123..4fc6365 100644 --- a/controls-core/src/commonMain/kotlin/space/kscience/controls/api/Device.kt +++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/api/Device.kt @@ -9,10 +9,21 @@ import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import space.kscience.controls.api.Device.Companion.DEVICE_TARGET import space.kscience.dataforge.context.ContextAware +import space.kscience.dataforge.context.info +import space.kscience.dataforge.context.logger import space.kscience.dataforge.meta.Meta +import space.kscience.dataforge.misc.DFExperimental import space.kscience.dataforge.misc.Type import space.kscience.dataforge.names.Name +/** + * A lifecycle state of a device + */ +public enum class DeviceLifecycleState{ + INIT, + OPEN, + CLOSED +} /** * General interface describing a managed Device. @@ -79,12 +90,16 @@ public interface Device : AutoCloseable, ContextAware, CoroutineScope { public suspend fun open(): Unit = Unit /** - * Close and terminate the device. This function does not wait for device to be closed. + * Close and terminate the device. This function does not wait for the device to be closed. */ override fun close() { + logger.info { "Device $this is closed" } cancel("The device is closed") } + @DFExperimental + public val lifecycleState: DeviceLifecycleState + public companion object { public const val DEVICE_TARGET: String = "device" } diff --git a/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/DeviceBase.kt b/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/DeviceBase.kt index 034485f..56f5aa9 100644 --- a/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/DeviceBase.kt +++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/DeviceBase.kt @@ -1,15 +1,16 @@ package space.kscience.controls.spec -import kotlinx.coroutines.Job -import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.* import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import space.kscience.controls.api.* import space.kscience.dataforge.context.Context -import space.kscience.dataforge.context.Global +import space.kscience.dataforge.context.error +import space.kscience.dataforge.context.logger import space.kscience.dataforge.meta.Meta +import space.kscience.dataforge.misc.DFExperimental import kotlin.coroutines.CoroutineContext @@ -37,7 +38,7 @@ private suspend fun DeviceActionSpec.executeWithMeta * A base abstractions for [Device], introducing specifications for properties */ public abstract class DeviceBase( - override val context: Context = Global, + final override val context: Context, override val meta: Meta = Meta.EMPTY, ) : Device { @@ -58,9 +59,16 @@ public abstract class DeviceBase( get() = actions.values.map { it.descriptor } override val coroutineContext: CoroutineContext by lazy { - context.coroutineContext + SupervisorJob(context.coroutineContext[Job]) + context.newCoroutineContext( + SupervisorJob(context.coroutineContext[Job]) + + CoroutineName("Device $this") + + CoroutineExceptionHandler { _, throwable -> + logger.error(throwable) { "Exception in device $this job" } + } + ) } + /** * Logical state store */ @@ -149,5 +157,23 @@ public abstract class DeviceBase( return spec.executeWithMeta(self, argument ?: Meta.EMPTY) } + @DFExperimental + override var lifecycleState: DeviceLifecycleState = DeviceLifecycleState.INIT + protected set + + @OptIn(DFExperimental::class) + override suspend fun open() { + super.open() + lifecycleState = DeviceLifecycleState.OPEN + } + + @OptIn(DFExperimental::class) + override fun close() { + lifecycleState = DeviceLifecycleState.CLOSED + super.close() + } + + abstract override fun toString(): String + } diff --git a/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/DeviceBySpec.kt b/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/DeviceBySpec.kt index e8a071c..9309224 100644 --- a/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/DeviceBySpec.kt +++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/DeviceBySpec.kt @@ -2,7 +2,6 @@ package space.kscience.controls.spec import space.kscience.controls.api.Device import space.kscience.dataforge.context.Context -import space.kscience.dataforge.context.Global import space.kscience.dataforge.meta.Meta /** @@ -11,7 +10,7 @@ import space.kscience.dataforge.meta.Meta */ public open class DeviceBySpec( public val spec: DeviceSpec, - context: Context = Global, + context: Context, meta: Meta = Meta.EMPTY, ) : DeviceBase(context, meta) { override val properties: Map> get() = spec.properties @@ -26,4 +25,6 @@ public open class DeviceBySpec( self.onClose() super.close() } + + override fun toString(): String = "Device(spec=$spec)" } \ No newline at end of file diff --git a/controls-modbus/src/main/kotlin/space/kscience/controls/modbus/DeviceProcessImage.kt b/controls-modbus/src/main/kotlin/space/kscience/controls/modbus/DeviceProcessImage.kt index 47acb4a..3352f1a 100644 --- a/controls-modbus/src/main/kotlin/space/kscience/controls/modbus/DeviceProcessImage.kt +++ b/controls-modbus/src/main/kotlin/space/kscience/controls/modbus/DeviceProcessImage.kt @@ -4,6 +4,7 @@ import com.ghgande.j2mod.modbus.procimg.* import io.ktor.utils.io.core.buildPacket import io.ktor.utils.io.core.readByteBuffer import io.ktor.utils.io.core.writeShort +import kotlinx.coroutines.launch import space.kscience.controls.api.Device import space.kscience.controls.spec.DevicePropertySpec import space.kscience.controls.spec.WritableDevicePropertySpec @@ -11,138 +12,206 @@ import space.kscience.controls.spec.set import space.kscience.controls.spec.useProperty -public class DeviceToModbusMapping private constructor( - private val mapping: Map, ModbusRegistryKey>, -) : Map, ModbusRegistryKey> by mapping { - public class Builder { - private val mapping = HashMap, ModbusRegistryKey>() +public class DeviceProcessImageBuilder( + private val device: D, + public val image: ProcessImageImplementation, +) { - public fun bind(propertySpec: DevicePropertySpec, key: ModbusRegistryKey.DiscreteInput) { - mapping[propertySpec] = key - } - - public fun bind(propertySpec: WritableDevicePropertySpec, key: ModbusRegistryKey.Coil) { - mapping[propertySpec] = key - } - - public fun bind(propertySpec: DevicePropertySpec, key: ModbusRegistryKey.InputRegister) { - mapping[propertySpec] = key - } - - public fun bind(propertySpec: WritableDevicePropertySpec, key: ModbusRegistryKey.HoldingRegister) { - mapping[propertySpec] = key - } - - public fun bind(propertySpec: DevicePropertySpec, key: ModbusRegistryKey.InputRange) { - mapping[propertySpec] = key - } - - public fun bind(propertySpec: WritableDevicePropertySpec, key: ModbusRegistryKey.HoldingRange) { - mapping[propertySpec] = key - } - - public fun build(): DeviceToModbusMapping = DeviceToModbusMapping(mapping) + public fun bind( + key: ModbusRegistryKey.Coil, + block: D.(ObservableDigitalOut) -> Unit = {}, + ): ObservableDigitalOut { + val coil = ObservableDigitalOut() + device.block(coil) + image.addDigitalOut(key.address, coil) + return coil } + + public fun bind( + key: ModbusRegistryKey.Coil, + propertySpec: WritableDevicePropertySpec, + ): ObservableDigitalOut = bind(key) { coil -> + coil.addObserver { _, _ -> + device[propertySpec] = coil.isSet + } + device.useProperty(propertySpec) { value -> + coil.set(value) + } + } + + public fun bind( + key: ModbusRegistryKey.DiscreteInput, + block: D.(SimpleDigitalIn) -> Unit = {}, + ): DigitalIn { + val input = SimpleDigitalIn() + device.block(input) + image.addDigitalIn(key.address, input) + return input + } + + public fun bind( + key: ModbusRegistryKey.DiscreteInput, + propertySpec: DevicePropertySpec, + ): DigitalIn = bind(key) { input -> + device.useProperty(propertySpec) { value -> + input.set(value) + } + } + + public fun bind( + key: ModbusRegistryKey.InputRegister, + block: D.(SimpleInputRegister) -> Unit = {}, + ): SimpleInputRegister { + val input = SimpleInputRegister() + device.block(input) + image.addInputRegister(key.address, input) + return input + } + + public fun bind( + key: ModbusRegistryKey.InputRegister, + propertySpec: DevicePropertySpec, + ): SimpleInputRegister = bind(key) { input -> + device.useProperty(propertySpec) { value -> + input.setValue(value) + } + } + + public fun bind( + key: ModbusRegistryKey.HoldingRegister, + block: D.(ObservableRegister) -> Unit = {}, + ): ObservableRegister { + val register = ObservableRegister() + device.block(register) + image.addRegister(key.address, register) + return register + } + + public fun bind( + key: ModbusRegistryKey.HoldingRegister, + propertySpec: WritableDevicePropertySpec, + ): ObservableRegister = bind(key) { register -> + register.addObserver { _, _ -> + device[propertySpec] = register.toShort() + } + device.useProperty(propertySpec) { value -> + register.setValue(value) + } + } + + public fun bind(key: ModbusRegistryKey.InputRange, propertySpec: DevicePropertySpec) { + val registers = List(key.count) { + SimpleInputRegister() + } + + registers.forEachIndexed { index, register -> + image.addInputRegister(key.address + index, register) + } + + device.useProperty(propertySpec) { value -> + val packet = buildPacket { + key.format.writeObject(this, value) + }.readByteBuffer() + registers.forEachIndexed { index, register -> + register.setValue(packet.getShort(index * 2)) + } + } + } + + public fun bind(key: ModbusRegistryKey.HoldingRange, propertySpec: WritableDevicePropertySpec) { + val registers = List(key.count) { + ObservableRegister() + } + registers.forEachIndexed { index, register -> + register.addObserver { _, _ -> + val packet = buildPacket { + registers.forEach { value -> + writeShort(value.toShort()) + } + } + device[propertySpec] = key.format.readObject(packet) + } + image.addRegister(key.address + index, register) + } + + device.useProperty(propertySpec) { value -> + val packet = buildPacket { + key.format.writeObject(this, value) + }.readByteBuffer() + registers.forEachIndexed { index, observableRegister -> + observableRegister.setValue(packet.getShort(index * 2)) + } + } + } + + public fun bindAction( + key: ModbusRegistryKey.Coil, + action: suspend D.(Boolean) -> Unit, + ): ObservableDigitalOut { + val coil = ObservableDigitalOut() + coil.addObserver { _, _ -> + device.launch { + device.action(coil.isSet) + } + } + image.addDigitalOut(key.address, coil) + return coil + } + + public fun bindAction( + key: ModbusRegistryKey.HoldingRegister, + action: suspend D.(Short) -> Unit, + ): ObservableRegister { + val register = ObservableRegister() + register.addObserver { _, _ -> + + with(device) { + launch { + action(register.toShort()) + } + } + } + image.addRegister(key.address, register) + return register + } + + public fun bindAction( + key: ModbusRegistryKey.HoldingRange, + action: suspend D.(T) -> Unit, + ): List { + val registers = List(key.count) { + ObservableRegister() + } + registers.forEachIndexed { index, register -> + register.addObserver { _, _ -> + val packet = buildPacket { + registers.forEach { value -> + writeShort(value.toShort()) + } + } + device.launch { + device.action(key.format.readObject(packet)) + } + } + image.addRegister(key.address + index, register) + } + + return registers + } + } -public inline fun DeviceToModbusMapping(block: DeviceToModbusMapping.Builder.()->Unit): DeviceToModbusMapping = - DeviceToModbusMapping.Builder().apply(block).build() -@Suppress("UNCHECKED_CAST") -public fun D.toProcessImage(mapping: DeviceToModbusMapping): ProcessImage { +public fun D.bindProcessImage( + openOnBind: Boolean = true, + binding: DeviceProcessImageBuilder.() -> Unit, +): ProcessImage { val image = SimpleProcessImage() - mapping.forEach { (spec, key) -> - when (key) { - is ModbusRegistryKey.Coil -> { - spec as WritableDevicePropertySpec - val coil = ObservableDigitalOut() - coil.addObserver { _, _ -> - set(spec, coil.isSet) - } - image.setDigitalOut(key.address, coil) - useProperty(spec) { value -> - coil.set(value) - } - } - - is ModbusRegistryKey.DiscreteInput -> { - spec as DevicePropertySpec - val input = SimpleDigitalIn() - image.setDigitalIn(key.address, input) - useProperty(spec) { value -> - input.set(value) - } - } - - is ModbusRegistryKey.HoldingRegister -> { - spec as WritableDevicePropertySpec - val register = ObservableRegister() - register.addObserver { _, _ -> - set(spec, register.toShort()) - } - image.setRegister(key.address, register) - useProperty(spec) { value -> - register.setValue(value) - } - } - - is ModbusRegistryKey.InputRegister -> { - spec as DevicePropertySpec - val input = SimpleInputRegister() - image.setRegister(key.address, input) - useProperty(spec) { value -> - input.setValue(value) - } - } - - is ModbusRegistryKey.HoldingRange<*> -> { - spec as WritableDevicePropertySpec - key as ModbusRegistryKey.HoldingRange - val registers = List(key.count) { - ObservableRegister() - } - registers.forEachIndexed { index, register -> - register.addObserver { _, _ -> - val packet = buildPacket { - registers.forEach { value -> - writeShort(value.toShort()) - } - } - set(spec, key.format.readObject(packet)) - } - image.setRegister(key.address + index, register) - } - - useProperty(spec) { value -> - val packet = buildPacket { - key.format.writeObject(this, value) - }.readByteBuffer() - registers.forEachIndexed { index, observableRegister -> - observableRegister.setValue(packet.getShort(index * 2)) - } - } - } - - is ModbusRegistryKey.InputRange<*> -> { - spec as DevicePropertySpec - key as ModbusRegistryKey.InputRange - val registers = List(key.count) { - SimpleInputRegister() - } - - useProperty(spec) { value -> - val packet = buildPacket { - key.format.writeObject(this, value) - }.readByteBuffer() - registers.forEachIndexed { index, register -> - register.setValue(packet.getShort(index * 2)) - } - } - } + DeviceProcessImageBuilder(this, image).apply(binding) + if (openOnBind) { + launch { + open() } } return image -} - -public inline fun D.toProcessImage(block: DeviceToModbusMapping.Builder.()->Unit): ProcessImage = - toProcessImage(DeviceToModbusMapping(block)) \ No newline at end of file +} \ No newline at end of file diff --git a/controls-modbus/src/main/kotlin/space/kscience/controls/modbus/ModbusDevice.kt b/controls-modbus/src/main/kotlin/space/kscience/controls/modbus/ModbusDevice.kt index 1c442aa..ea4330c 100644 --- a/controls-modbus/src/main/kotlin/space/kscience/controls/modbus/ModbusDevice.kt +++ b/controls-modbus/src/main/kotlin/space/kscience/controls/modbus/ModbusDevice.kt @@ -3,7 +3,7 @@ package space.kscience.controls.modbus import com.ghgande.j2mod.modbus.facade.AbstractModbusMaster import com.ghgande.j2mod.modbus.procimg.InputRegister import com.ghgande.j2mod.modbus.procimg.Register -import com.ghgande.j2mod.modbus.procimg.SimpleRegister +import com.ghgande.j2mod.modbus.procimg.SimpleInputRegister import com.ghgande.j2mod.modbus.util.BitVector import io.ktor.utils.io.core.ByteReadPacket import io.ktor.utils.io.core.buildPacket @@ -73,7 +73,7 @@ public interface ModbusDevice : Device { val buffer = buildPacket { format.writeObject(this, value) }.readByteBuffer() - writeHoldingRegisters(address,buffer) + writeHoldingRegisters(address, buffer) } } @@ -81,36 +81,36 @@ public interface ModbusDevice : Device { /** * Read multiple sequential modbus coils (bit-values) */ -public fun ModbusDevice.readCoils(ref: Int, count: Int): BitVector = - master.readCoils(clientId, ref, count) +public fun ModbusDevice.readCoils(address: Int, count: Int): BitVector = + master.readCoils(clientId, address, count) -public fun ModbusDevice.readCoil(ref: Int): Boolean = - master.readCoils(clientId, ref, 1).getBit(0) +public fun ModbusDevice.readCoil(address: Int): Boolean = + master.readCoils(clientId, address, 1).getBit(0) -public fun ModbusDevice.writeCoils(ref: Int, values: BooleanArray) { +public fun ModbusDevice.writeCoils(address: Int, values: BooleanArray) { val bitVector = BitVector(values.size) values.forEachIndexed { index, value -> bitVector.setBit(index, value) } - master.writeMultipleCoils(clientId, ref, bitVector) + master.writeMultipleCoils(clientId, address, bitVector) } -public fun ModbusDevice.writeCoil(ref: Int, value: Boolean) { - master.writeCoil(clientId, ref, value) +public fun ModbusDevice.writeCoil(address: Int, value: Boolean) { + master.writeCoil(clientId, address, value) } public fun ModbusDevice.writeCoil(key: ModbusRegistryKey.Coil, value: Boolean) { master.writeCoil(clientId, key.address, value) } -public fun ModbusDevice.readInputDiscretes(ref: Int, count: Int): BitVector = - master.readInputDiscretes(clientId, ref, count) +public fun ModbusDevice.readInputDiscretes(address: Int, count: Int): BitVector = + master.readInputDiscretes(clientId, address, count) -public fun ModbusDevice.readInputDiscrete(ref: Int): Boolean = - master.readInputDiscretes(clientId, ref, 1).getBit(0) +public fun ModbusDevice.readInputDiscrete(address: Int): Boolean = + master.readInputDiscretes(clientId, address, 1).getBit(0) -public fun ModbusDevice.readInputRegisters(ref: Int, count: Int): List = - master.readInputRegisters(clientId, ref, count).toList() +public fun ModbusDevice.readInputRegisters(address: Int, count: Int): List = + master.readInputRegisters(clientId, address, count).toList() private fun Array.toBuffer(): ByteBuffer { val buffer: ByteBuffer = ByteBuffer.allocate(size * 2) @@ -128,79 +128,82 @@ private fun Array.toPacket(): ByteReadPacket = buildPacket { } } -public fun ModbusDevice.readInputRegistersToBuffer(ref: Int, count: Int): ByteBuffer = - master.readInputRegisters(clientId, ref, count).toBuffer() +public fun ModbusDevice.readInputRegistersToBuffer(address: Int, count: Int): ByteBuffer = + master.readInputRegisters(clientId, address, count).toBuffer() -public fun ModbusDevice.readInputRegistersToPacket(ref: Int, count: Int): ByteReadPacket = - master.readInputRegisters(clientId, ref, count).toPacket() +public fun ModbusDevice.readInputRegistersToPacket(address: Int, count: Int): ByteReadPacket = + master.readInputRegisters(clientId, address, count).toPacket() -public fun ModbusDevice.readDoubleInput(ref: Int): Double = - readInputRegistersToBuffer(ref, Double.SIZE_BYTES).getDouble() +public fun ModbusDevice.readDoubleInput(address: Int): Double = + readInputRegistersToBuffer(address, Double.SIZE_BYTES).getDouble() -public fun ModbusDevice.readInputRegister(ref: Int): Short = - readInputRegisters(ref, 1).first().toShort() +public fun ModbusDevice.readInputRegister(address: Int): Short = + readInputRegisters(address, 1).first().toShort() -public fun ModbusDevice.readHoldingRegisters(ref: Int, count: Int): List = - master.readMultipleRegisters(clientId, ref, count).toList() +public fun ModbusDevice.readHoldingRegisters(address: Int, count: Int): List = + master.readMultipleRegisters(clientId, address, count).toList() /** * Read a number of registers to a [ByteBuffer] - * @param ref address of a register + * @param address of a register * @param count number of 2-bytes registers to read. Buffer size is 2*[count] */ -public fun ModbusDevice.readHoldingRegistersToBuffer(ref: Int, count: Int): ByteBuffer = - master.readMultipleRegisters(clientId, ref, count).toBuffer() +public fun ModbusDevice.readHoldingRegistersToBuffer(address: Int, count: Int): ByteBuffer = + master.readMultipleRegisters(clientId, address, count).toBuffer() -public fun ModbusDevice.readHoldingRegistersToPacket(ref: Int, count: Int): ByteReadPacket = - master.readMultipleRegisters(clientId, ref, count).toPacket() +public fun ModbusDevice.readHoldingRegistersToPacket(address: Int, count: Int): ByteReadPacket = + master.readMultipleRegisters(clientId, address, count).toPacket() -public fun ModbusDevice.readDoubleRegister(ref: Int): Double = - readHoldingRegistersToBuffer(ref, Double.SIZE_BYTES).getDouble() +public fun ModbusDevice.readDoubleRegister(address: Int): Double = + readHoldingRegistersToBuffer(address, Double.SIZE_BYTES).getDouble() -public fun ModbusDevice.readHoldingRegister(ref: Int): Short = - readHoldingRegisters(ref, 1).first().toShort() +public fun ModbusDevice.readHoldingRegister(address: Int): Short = + readHoldingRegisters(address, 1).first().toShort() -public fun ModbusDevice.writeHoldingRegisters(ref: Int, values: ShortArray): Int = +public fun ModbusDevice.writeHoldingRegisters(address: Int, values: ShortArray): Int = master.writeMultipleRegisters( clientId, - ref, - Array(values.size) { SimpleRegister().apply { setValue(values[it]) } } + address, + Array(values.size) { SimpleInputRegister(values[it].toInt()) } ) -public fun ModbusDevice.writeHoldingRegister(ref: Int, value: Short): Int = +public fun ModbusDevice.writeHoldingRegister(address: Int, value: Short): Int = master.writeSingleRegister( clientId, - ref, - SimpleRegister().apply { setValue(value) } + address, + SimpleInputRegister(value.toInt()) ) -public fun ModbusDevice.writeHoldingRegisters(ref: Int, buffer: ByteBuffer): Int { +public fun ModbusDevice.writeHoldingRegister(key: ModbusRegistryKey.HoldingRegister, value: Short): Int = + writeHoldingRegister(key.address, value) + +public fun ModbusDevice.writeHoldingRegisters(address: Int, buffer: ByteBuffer): Int { val array: ShortArray = ShortArray(buffer.limit().floorDiv(2)) { buffer.getShort(it * 2) } - return writeHoldingRegisters(ref, array) + return writeHoldingRegisters(address, array) } -public fun ModbusDevice.writeShortRegister(ref: Int, value: Short) { - master.writeSingleRegister(ref, SimpleRegister().apply { setValue(value) }) +public fun ModbusDevice.writeShortRegister(address: Int, value: Short) { + master.writeSingleRegister(address, SimpleInputRegister(value.toInt())) } public fun ModbusDevice.modbusRegister( - ref: Int, + address: Int, ): ReadWriteProperty = object : ReadWriteProperty { - override fun getValue(thisRef: ModbusDevice, property: KProperty<*>): Short = readHoldingRegister(ref) + override fun getValue(thisRef: ModbusDevice, property: KProperty<*>): Short = readHoldingRegister(address) override fun setValue(thisRef: ModbusDevice, property: KProperty<*>, value: Short) { - writeHoldingRegister(ref, value) + writeHoldingRegister(address, value) } } public fun ModbusDevice.modbusDoubleRegister( - ref: Int, + address: Int, ): ReadWriteProperty = object : ReadWriteProperty { - override fun getValue(thisRef: ModbusDevice, property: KProperty<*>): Double = readDoubleRegister(ref) + override fun getValue(thisRef: ModbusDevice, property: KProperty<*>): Double = readDoubleRegister(address) override fun setValue(thisRef: ModbusDevice, property: KProperty<*>, value: Double) { val buffer = ByteBuffer.allocate(Double.SIZE_BYTES).apply { putDouble(value) } - writeHoldingRegisters(ref, buffer) + writeHoldingRegisters(address, buffer) } } diff --git a/controls-modbus/src/main/kotlin/space/kscience/controls/modbus/ModbusDeviceBySpec.kt b/controls-modbus/src/main/kotlin/space/kscience/controls/modbus/ModbusDeviceBySpec.kt index e1acd4b..1956e40 100644 --- a/controls-modbus/src/main/kotlin/space/kscience/controls/modbus/ModbusDeviceBySpec.kt +++ b/controls-modbus/src/main/kotlin/space/kscience/controls/modbus/ModbusDeviceBySpec.kt @@ -17,8 +17,21 @@ public open class ModbusDeviceBySpec( spec: DeviceSpec, override val clientId: Int, override val master: AbstractModbusMaster, + private val disposeMasterOnClose: Boolean = true, meta: Meta = Meta.EMPTY, -) : ModbusDevice, DeviceBySpec(spec, context, meta) +) : ModbusDevice, DeviceBySpec(spec, context, meta){ + override suspend fun open() { + master.connect() + super.open() + } + + override fun close() { + if(disposeMasterOnClose){ + master.disconnect() + } + super.close() + } +} public class ModbusHub( diff --git a/controls-modbus/src/main/kotlin/space/kscience/controls/modbus/ModbusRegistryMap.kt b/controls-modbus/src/main/kotlin/space/kscience/controls/modbus/ModbusRegistryMap.kt index 22f2619..a6fc894 100644 --- a/controls-modbus/src/main/kotlin/space/kscience/controls/modbus/ModbusRegistryMap.kt +++ b/controls-modbus/src/main/kotlin/space/kscience/controls/modbus/ModbusRegistryMap.kt @@ -7,63 +7,48 @@ public sealed class ModbusRegistryKey { public abstract val address: Int public open val count: Int = 1 + /** * Read-only boolean value */ - public data class Coil(override val address: Int) : ModbusRegistryKey() { - init { - require(address in 1..9999) { "Coil address must be in 1..9999 range" } - } - } + public data class Coil(override val address: Int) : ModbusRegistryKey() /** * Read-write boolean value */ - public data class DiscreteInput(override val address: Int) : ModbusRegistryKey() { - init { - require(address in 10001..19999) { "DiscreteInput address must be in 10001..19999 range" } - } - } + public data class DiscreteInput(override val address: Int) : ModbusRegistryKey() /** * Read-only binary value */ - public data class InputRegister(override val address: Int) : ModbusRegistryKey() { - init { - require(address in 20001..29999) { "InputRegister address must be in 20001..29999 range" } - } + public open class InputRegister(override val address: Int) : ModbusRegistryKey() { + override fun toString(): String = "InputRegister(address=$address)" } - public data class InputRange( - override val address: Int, + public class InputRange( + address: Int, override val count: Int, public val format: IOFormat, - ) : ModbusRegistryKey() { + ) : InputRegister(address) { public val endAddress: Int get() = address + count + override fun toString(): String = "InputRange(count=$count, format=$format)" + - init { - require(address in 20001..29999) { "InputRange begin address is $address, but must be in 20001..29999 range" } - require(endAddress in 20001..29999) { "InputRange end address is ${endAddress}, but must be in 20001..29999 range" } - } } - public data class HoldingRegister(override val address: Int) : ModbusRegistryKey() { - init { - require(address in 30001..39999) { "HoldingRegister address must be in 30001..39999 range" } - } + public open class HoldingRegister(override val address: Int) : ModbusRegistryKey() { + override fun toString(): String = "HoldingRegister(address=$address)" } - public data class HoldingRange( - override val address: Int, + public class HoldingRange( + address: Int, override val count: Int, public val format: IOFormat, - ) : ModbusRegistryKey() { + ) : HoldingRegister(address) { public val endAddress: Int get() = address + count + override fun toString(): String = "HoldingRange(count=$count, format=$format)" + - init { - require(address in 30001..39999) { "HoldingRange begin address is $address, but must be in 30001..39999 range" } - require(endAddress in 30001..39999) { "HoldingRange end address is ${endAddress}, but must be in 30001..39999 range" } - } } } @@ -81,15 +66,10 @@ public abstract class ModbusRegistryMap { protected fun coil(address: Int, description: String = ""): ModbusRegistryKey.Coil = register(ModbusRegistryKey.Coil(address), description) - protected fun coilByOffset(offset: Int, description: String = ""): ModbusRegistryKey.Coil = - register(ModbusRegistryKey.Coil(offset), description) protected fun discrete(address: Int, description: String = ""): ModbusRegistryKey.DiscreteInput = register(ModbusRegistryKey.DiscreteInput(address), description) - protected fun discreteByOffset(offset: Int, description: String = ""): ModbusRegistryKey.DiscreteInput = - register(ModbusRegistryKey.DiscreteInput(10000 + offset), description) - protected fun input(address: Int, description: String = ""): ModbusRegistryKey.InputRegister = register(ModbusRegistryKey.InputRegister(address), description) @@ -101,17 +81,6 @@ public abstract class ModbusRegistryMap { ): ModbusRegistryKey.InputRange = register(ModbusRegistryKey.InputRange(address, count, reader), description) - protected fun inputByOffset(offset: Int, description: String = ""): ModbusRegistryKey.InputRegister = - register(ModbusRegistryKey.InputRegister(20000 + offset), description) - - protected fun inputByOffset( - offset: Int, - count: Int, - reader: IOFormat, - description: String = "", - ): ModbusRegistryKey.InputRange = - register(ModbusRegistryKey.InputRange(20000 + offset, count, reader), description) - protected fun register(address: Int, description: String = ""): ModbusRegistryKey.HoldingRegister = register(ModbusRegistryKey.HoldingRegister(address), description) @@ -123,40 +92,70 @@ public abstract class ModbusRegistryMap { ): ModbusRegistryKey.HoldingRange = register(ModbusRegistryKey.HoldingRange(address, count, format), description) - protected fun registerByOffset(offset: Int, description: String = ""): ModbusRegistryKey.HoldingRegister = - register(ModbusRegistryKey.HoldingRegister(30000 + offset), description) - - protected fun registerByOffset( - offset: Int, - count: Int, - format: IOFormat, - description: String = "", - ): ModbusRegistryKey.HoldingRange = - register(ModbusRegistryKey.HoldingRange(offset + 30000, count, format), description) - public companion object { public fun validate(map: ModbusRegistryMap) { - map.entries.keys.sortedBy { it.address }.zipWithNext().forEach { (l, r) -> - if (l.address + l.count > r.address) error("Key $l overlaps with key $r") + var lastCoil: ModbusRegistryKey.Coil? = null + var lastDiscreteInput: ModbusRegistryKey.DiscreteInput? = null + var lastInput: ModbusRegistryKey.InputRegister? = null + var lastRegister: ModbusRegistryKey.HoldingRegister? = null + map.entries.keys.sortedBy { it.address }.forEach { key -> + when (key) { + is ModbusRegistryKey.Coil -> if (lastCoil?.let { key.address >= it.address + it.count } != false) { + lastCoil = key + } else { + error("Key $lastCoil overlaps with key $key") + } + + is ModbusRegistryKey.DiscreteInput -> if (lastDiscreteInput?.let { key.address >= it.address + it.count } != false) { + lastDiscreteInput = key + } else { + error("Key $lastDiscreteInput overlaps with key $key") + } + + is ModbusRegistryKey.InputRegister -> if (lastInput?.let { key.address >= it.address + it.count } != false) { + lastInput = key + } else { + error("Key $lastInput overlaps with key $key") + } + + is ModbusRegistryKey.HoldingRegister -> if (lastRegister?.let { key.address >= it.address + it.count } != false) { + lastRegister = key + } else { + error("Key $lastRegister overlaps with key $key") + } + } } } + private val ModbusRegistryKey.sectionNumber + get() = when (this) { + is ModbusRegistryKey.Coil -> 1 + is ModbusRegistryKey.DiscreteInput -> 2 + is ModbusRegistryKey.HoldingRegister -> 4 + is ModbusRegistryKey.InputRegister -> 3 + } + public fun print(map: ModbusRegistryMap, to: Appendable = System.out) { validate(map) - map.entries.entries.sortedBy { it.key.address }.forEach { (key, description) -> - val typeString = when (key) { - is ModbusRegistryKey.Coil -> "Coil" - is ModbusRegistryKey.DiscreteInput -> "Discrete" - is ModbusRegistryKey.HoldingRange<*>, is ModbusRegistryKey.HoldingRegister -> "Register" - is ModbusRegistryKey.InputRange<*>, is ModbusRegistryKey.InputRegister -> "Input" + map.entries.entries + .sortedWith( + Comparator.comparingInt?> { it.key.sectionNumber } + .thenComparingInt { it.key.address } + ) + .forEach { (key, description) -> + val typeString = when (key) { + is ModbusRegistryKey.Coil -> "Coil" + is ModbusRegistryKey.DiscreteInput -> "Discrete" + is ModbusRegistryKey.HoldingRegister -> "Register" + is ModbusRegistryKey.InputRegister -> "Input" + } + val rangeString = if (key.count == 1) { + key.address.toString() + } else { + "${key.address} - ${key.address + key.count}" + } + to.appendLine("${typeString}\t$rangeString\t$description") } - val rangeString = if (key.count == 1) { - key.address.toString() - } else { - "${key.address} - ${key.address + key.count}" - } - to.appendLine("${typeString}\t$rangeString\t$description") - } } } }