Modbus revision acording to modern standards

This commit is contained in:
Alexander Nozik 2023-06-10 16:17:54 +03:00
parent 33e1aafa01
commit 20951a0b0f
7 changed files with 386 additions and 260 deletions

View File

@ -9,10 +9,21 @@ import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import space.kscience.controls.api.Device.Companion.DEVICE_TARGET import space.kscience.controls.api.Device.Companion.DEVICE_TARGET
import space.kscience.dataforge.context.ContextAware 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.meta.Meta
import space.kscience.dataforge.misc.DFExperimental
import space.kscience.dataforge.misc.Type import space.kscience.dataforge.misc.Type
import space.kscience.dataforge.names.Name 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. * General interface describing a managed Device.
@ -79,12 +90,16 @@ public interface Device : AutoCloseable, ContextAware, CoroutineScope {
public suspend fun open(): Unit = Unit 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() { override fun close() {
logger.info { "Device $this is closed" }
cancel("The device is closed") cancel("The device is closed")
} }
@DFExperimental
public val lifecycleState: DeviceLifecycleState
public companion object { public companion object {
public const val DEVICE_TARGET: String = "device" public const val DEVICE_TARGET: String = "device"
} }

View File

@ -1,15 +1,16 @@
package space.kscience.controls.spec package space.kscience.controls.spec
import kotlinx.coroutines.Job import kotlinx.coroutines.*
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
import space.kscience.controls.api.* import space.kscience.controls.api.*
import space.kscience.dataforge.context.Context 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.meta.Meta
import space.kscience.dataforge.misc.DFExperimental
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
@ -37,7 +38,7 @@ private suspend fun <D : Device, I, O> DeviceActionSpec<D, I, O>.executeWithMeta
* A base abstractions for [Device], introducing specifications for properties * A base abstractions for [Device], introducing specifications for properties
*/ */
public abstract class DeviceBase<D : Device>( public abstract class DeviceBase<D : Device>(
override val context: Context = Global, final override val context: Context,
override val meta: Meta = Meta.EMPTY, override val meta: Meta = Meta.EMPTY,
) : Device { ) : Device {
@ -58,8 +59,15 @@ public abstract class DeviceBase<D : Device>(
get() = actions.values.map { it.descriptor } get() = actions.values.map { it.descriptor }
override val coroutineContext: CoroutineContext by lazy { 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 * Logical state store
@ -149,5 +157,23 @@ public abstract class DeviceBase<D : Device>(
return spec.executeWithMeta(self, argument ?: Meta.EMPTY) 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
} }

View File

@ -2,7 +2,6 @@ package space.kscience.controls.spec
import space.kscience.controls.api.Device import space.kscience.controls.api.Device
import space.kscience.dataforge.context.Context import space.kscience.dataforge.context.Context
import space.kscience.dataforge.context.Global
import space.kscience.dataforge.meta.Meta import space.kscience.dataforge.meta.Meta
/** /**
@ -11,7 +10,7 @@ import space.kscience.dataforge.meta.Meta
*/ */
public open class DeviceBySpec<D : Device>( public open class DeviceBySpec<D : Device>(
public val spec: DeviceSpec<in D>, public val spec: DeviceSpec<in D>,
context: Context = Global, context: Context,
meta: Meta = Meta.EMPTY, meta: Meta = Meta.EMPTY,
) : DeviceBase<D>(context, meta) { ) : DeviceBase<D>(context, meta) {
override val properties: Map<String, DevicePropertySpec<D, *>> get() = spec.properties override val properties: Map<String, DevicePropertySpec<D, *>> get() = spec.properties
@ -26,4 +25,6 @@ public open class DeviceBySpec<D : Device>(
self.onClose() self.onClose()
super.close() super.close()
} }
override fun toString(): String = "Device(spec=$spec)"
} }

View File

@ -4,6 +4,7 @@ import com.ghgande.j2mod.modbus.procimg.*
import io.ktor.utils.io.core.buildPacket import io.ktor.utils.io.core.buildPacket
import io.ktor.utils.io.core.readByteBuffer import io.ktor.utils.io.core.readByteBuffer
import io.ktor.utils.io.core.writeShort import io.ktor.utils.io.core.writeShort
import kotlinx.coroutines.launch
import space.kscience.controls.api.Device import space.kscience.controls.api.Device
import space.kscience.controls.spec.DevicePropertySpec import space.kscience.controls.spec.DevicePropertySpec
import space.kscience.controls.spec.WritableDevicePropertySpec import space.kscience.controls.spec.WritableDevicePropertySpec
@ -11,93 +12,113 @@ import space.kscience.controls.spec.set
import space.kscience.controls.spec.useProperty import space.kscience.controls.spec.useProperty
public class DeviceToModbusMapping<D : Device> private constructor( public class DeviceProcessImageBuilder<D : Device>(
private val mapping: Map<DevicePropertySpec<D, *>, ModbusRegistryKey>, private val device: D,
) : Map<DevicePropertySpec<D, *>, ModbusRegistryKey> by mapping { public val image: ProcessImageImplementation,
public class Builder<D : Device> { ) {
private val mapping = HashMap<DevicePropertySpec<D, *>, ModbusRegistryKey>()
public fun bind(propertySpec: DevicePropertySpec<D, Boolean>, key: ModbusRegistryKey.DiscreteInput) { public fun bind(
mapping[propertySpec] = key key: ModbusRegistryKey.Coil,
} block: D.(ObservableDigitalOut) -> Unit = {},
): ObservableDigitalOut {
public fun bind(propertySpec: WritableDevicePropertySpec<D, Boolean>, key: ModbusRegistryKey.Coil) {
mapping[propertySpec] = key
}
public fun bind(propertySpec: DevicePropertySpec<D, Short>, key: ModbusRegistryKey.InputRegister) {
mapping[propertySpec] = key
}
public fun bind(propertySpec: WritableDevicePropertySpec<D, Short>, key: ModbusRegistryKey.HoldingRegister) {
mapping[propertySpec] = key
}
public fun <T> bind(propertySpec: DevicePropertySpec<D, T>, key: ModbusRegistryKey.InputRange<T>) {
mapping[propertySpec] = key
}
public fun <T> bind(propertySpec: WritableDevicePropertySpec<D, T>, key: ModbusRegistryKey.HoldingRange<T>) {
mapping[propertySpec] = key
}
public fun build(): DeviceToModbusMapping<D> = DeviceToModbusMapping(mapping)
}
}
public inline fun <D: Device> DeviceToModbusMapping(block: DeviceToModbusMapping.Builder<D>.()->Unit): DeviceToModbusMapping<D> =
DeviceToModbusMapping.Builder<D>().apply(block).build()
@Suppress("UNCHECKED_CAST")
public fun <D : Device> D.toProcessImage(mapping: DeviceToModbusMapping<D>): ProcessImage {
val image = SimpleProcessImage()
mapping.forEach { (spec, key) ->
when (key) {
is ModbusRegistryKey.Coil -> {
spec as WritableDevicePropertySpec<D, Boolean>
val coil = ObservableDigitalOut() val coil = ObservableDigitalOut()
coil.addObserver { _, _ -> device.block(coil)
set(spec, coil.isSet) image.addDigitalOut(key.address, coil)
return coil
} }
image.setDigitalOut(key.address, coil)
useProperty(spec) { value -> public fun bind(
key: ModbusRegistryKey.Coil,
propertySpec: WritableDevicePropertySpec<D, Boolean>,
): ObservableDigitalOut = bind(key) { coil ->
coil.addObserver { _, _ ->
device[propertySpec] = coil.isSet
}
device.useProperty(propertySpec) { value ->
coil.set(value) coil.set(value)
} }
} }
is ModbusRegistryKey.DiscreteInput -> { public fun bind(
spec as DevicePropertySpec<D, Boolean> key: ModbusRegistryKey.DiscreteInput,
block: D.(SimpleDigitalIn) -> Unit = {},
): DigitalIn {
val input = SimpleDigitalIn() val input = SimpleDigitalIn()
image.setDigitalIn(key.address, input) device.block(input)
useProperty(spec) { value -> image.addDigitalIn(key.address, input)
return input
}
public fun bind(
key: ModbusRegistryKey.DiscreteInput,
propertySpec: DevicePropertySpec<D, Boolean>,
): DigitalIn = bind(key) { input ->
device.useProperty(propertySpec) { value ->
input.set(value) input.set(value)
} }
} }
is ModbusRegistryKey.HoldingRegister -> { public fun bind(
spec as WritableDevicePropertySpec<D, Short> key: ModbusRegistryKey.InputRegister,
val register = ObservableRegister() block: D.(SimpleInputRegister) -> Unit = {},
register.addObserver { _, _ -> ): SimpleInputRegister {
set(spec, register.toShort()) val input = SimpleInputRegister()
} device.block(input)
image.setRegister(key.address, register) image.addInputRegister(key.address, input)
useProperty(spec) { value -> return input
register.setValue(value)
}
} }
is ModbusRegistryKey.InputRegister -> { public fun bind(
spec as DevicePropertySpec<D, Short> key: ModbusRegistryKey.InputRegister,
val input = SimpleInputRegister() propertySpec: DevicePropertySpec<D, Short>,
image.setRegister(key.address, input) ): SimpleInputRegister = bind(key) { input ->
useProperty(spec) { value -> device.useProperty(propertySpec) { value ->
input.setValue(value) input.setValue(value)
} }
} }
is ModbusRegistryKey.HoldingRange<*> -> { public fun bind(
spec as WritableDevicePropertySpec<D, Any?> key: ModbusRegistryKey.HoldingRegister,
key as ModbusRegistryKey.HoldingRange<Any?> 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<D, Short>,
): ObservableRegister = bind(key) { register ->
register.addObserver { _, _ ->
device[propertySpec] = register.toShort()
}
device.useProperty(propertySpec) { value ->
register.setValue(value)
}
}
public fun <T> bind(key: ModbusRegistryKey.InputRange<T>, propertySpec: DevicePropertySpec<D, T>) {
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 <T> bind(key: ModbusRegistryKey.HoldingRange<T>, propertySpec: WritableDevicePropertySpec<D, T>) {
val registers = List(key.count) { val registers = List(key.count) {
ObservableRegister() ObservableRegister()
} }
@ -108,12 +129,12 @@ public fun <D : Device> D.toProcessImage(mapping: DeviceToModbusMapping<D>): Pro
writeShort(value.toShort()) writeShort(value.toShort())
} }
} }
set(spec, key.format.readObject(packet)) device[propertySpec] = key.format.readObject(packet)
} }
image.setRegister(key.address + index, register) image.addRegister(key.address + index, register)
} }
useProperty(spec) { value -> device.useProperty(propertySpec) { value ->
val packet = buildPacket { val packet = buildPacket {
key.format.writeObject(this, value) key.format.writeObject(this, value)
}.readByteBuffer() }.readByteBuffer()
@ -123,26 +144,74 @@ public fun <D : Device> D.toProcessImage(mapping: DeviceToModbusMapping<D>): Pro
} }
} }
is ModbusRegistryKey.InputRange<*> -> { public fun bindAction(
spec as DevicePropertySpec<D, Any?> key: ModbusRegistryKey.Coil,
key as ModbusRegistryKey.InputRange<Any?> action: suspend D.(Boolean) -> Unit,
val registers = List(key.count) { ): ObservableDigitalOut {
SimpleInputRegister() val coil = ObservableDigitalOut()
coil.addObserver { _, _ ->
device.launch {
device.action(coil.isSet)
}
}
image.addDigitalOut(key.address, coil)
return coil
} }
useProperty(spec) { value -> public fun bindAction(
val packet = buildPacket { key: ModbusRegistryKey.HoldingRegister,
key.format.writeObject(this, value) action: suspend D.(Short) -> Unit,
}.readByteBuffer() ): ObservableRegister {
val register = ObservableRegister()
register.addObserver { _, _ ->
with(device) {
launch {
action(register.toShort())
}
}
}
image.addRegister(key.address, register)
return register
}
public fun <T> bindAction(
key: ModbusRegistryKey.HoldingRange<T>,
action: suspend D.(T) -> Unit,
): List<ObservableRegister> {
val registers = List(key.count) {
ObservableRegister()
}
registers.forEachIndexed { index, register -> registers.forEachIndexed { index, register ->
register.setValue(packet.getShort(index * 2)) 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 fun <D : Device> D.bindProcessImage(
openOnBind: Boolean = true,
binding: DeviceProcessImageBuilder<D>.() -> Unit,
): ProcessImage {
val image = SimpleProcessImage()
DeviceProcessImageBuilder(this, image).apply(binding)
if (openOnBind) {
launch {
open()
}
} }
return image return image
} }
public inline fun <D : Device> D.toProcessImage(block: DeviceToModbusMapping.Builder<D>.()->Unit): ProcessImage =
toProcessImage(DeviceToModbusMapping(block))

View File

@ -3,7 +3,7 @@ package space.kscience.controls.modbus
import com.ghgande.j2mod.modbus.facade.AbstractModbusMaster import com.ghgande.j2mod.modbus.facade.AbstractModbusMaster
import com.ghgande.j2mod.modbus.procimg.InputRegister 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.SimpleRegister 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 io.ktor.utils.io.core.ByteReadPacket
import io.ktor.utils.io.core.buildPacket import io.ktor.utils.io.core.buildPacket
@ -73,7 +73,7 @@ public interface ModbusDevice : Device {
val buffer = buildPacket { val buffer = buildPacket {
format.writeObject(this, value) format.writeObject(this, value)
}.readByteBuffer() }.readByteBuffer()
writeHoldingRegisters(address,buffer) writeHoldingRegisters(address, buffer)
} }
} }
@ -81,36 +81,36 @@ public interface ModbusDevice : Device {
/** /**
* Read multiple sequential modbus coils (bit-values) * Read multiple sequential modbus coils (bit-values)
*/ */
public fun ModbusDevice.readCoils(ref: Int, count: Int): BitVector = public fun ModbusDevice.readCoils(address: Int, count: Int): BitVector =
master.readCoils(clientId, ref, count) master.readCoils(clientId, address, count)
public fun ModbusDevice.readCoil(ref: Int): Boolean = public fun ModbusDevice.readCoil(address: Int): Boolean =
master.readCoils(clientId, ref, 1).getBit(0) 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) val bitVector = BitVector(values.size)
values.forEachIndexed { index, value -> values.forEachIndexed { index, value ->
bitVector.setBit(index, value) bitVector.setBit(index, value)
} }
master.writeMultipleCoils(clientId, ref, bitVector) master.writeMultipleCoils(clientId, address, bitVector)
} }
public fun ModbusDevice.writeCoil(ref: Int, value: Boolean) { public fun ModbusDevice.writeCoil(address: Int, value: Boolean) {
master.writeCoil(clientId, ref, value) master.writeCoil(clientId, address, value)
} }
public fun ModbusDevice.writeCoil(key: ModbusRegistryKey.Coil, value: Boolean) { public fun ModbusDevice.writeCoil(key: ModbusRegistryKey.Coil, value: Boolean) {
master.writeCoil(clientId, key.address, value) master.writeCoil(clientId, key.address, value)
} }
public fun ModbusDevice.readInputDiscretes(ref: Int, count: Int): BitVector = public fun ModbusDevice.readInputDiscretes(address: Int, count: Int): BitVector =
master.readInputDiscretes(clientId, ref, count) master.readInputDiscretes(clientId, address, count)
public fun ModbusDevice.readInputDiscrete(ref: Int): Boolean = public fun ModbusDevice.readInputDiscrete(address: Int): Boolean =
master.readInputDiscretes(clientId, ref, 1).getBit(0) master.readInputDiscretes(clientId, address, 1).getBit(0)
public fun ModbusDevice.readInputRegisters(ref: Int, count: Int): List<InputRegister> = public fun ModbusDevice.readInputRegisters(address: Int, count: Int): List<InputRegister> =
master.readInputRegisters(clientId, ref, count).toList() master.readInputRegisters(clientId, address, count).toList()
private fun Array<out InputRegister>.toBuffer(): ByteBuffer { private fun Array<out InputRegister>.toBuffer(): ByteBuffer {
val buffer: ByteBuffer = ByteBuffer.allocate(size * 2) val buffer: ByteBuffer = ByteBuffer.allocate(size * 2)
@ -128,79 +128,82 @@ private fun Array<out InputRegister>.toPacket(): ByteReadPacket = buildPacket {
} }
} }
public fun ModbusDevice.readInputRegistersToBuffer(ref: Int, count: Int): ByteBuffer = public fun ModbusDevice.readInputRegistersToBuffer(address: Int, count: Int): ByteBuffer =
master.readInputRegisters(clientId, ref, count).toBuffer() master.readInputRegisters(clientId, address, count).toBuffer()
public fun ModbusDevice.readInputRegistersToPacket(ref: Int, count: Int): ByteReadPacket = public fun ModbusDevice.readInputRegistersToPacket(address: Int, count: Int): ByteReadPacket =
master.readInputRegisters(clientId, ref, count).toPacket() master.readInputRegisters(clientId, address, count).toPacket()
public fun ModbusDevice.readDoubleInput(ref: Int): Double = public fun ModbusDevice.readDoubleInput(address: Int): Double =
readInputRegistersToBuffer(ref, Double.SIZE_BYTES).getDouble() readInputRegistersToBuffer(address, Double.SIZE_BYTES).getDouble()
public fun ModbusDevice.readInputRegister(ref: Int): Short = public fun ModbusDevice.readInputRegister(address: Int): Short =
readInputRegisters(ref, 1).first().toShort() readInputRegisters(address, 1).first().toShort()
public fun ModbusDevice.readHoldingRegisters(ref: Int, count: Int): List<Register> = public fun ModbusDevice.readHoldingRegisters(address: Int, count: Int): List<Register> =
master.readMultipleRegisters(clientId, ref, count).toList() master.readMultipleRegisters(clientId, address, count).toList()
/** /**
* Read a number of registers to a [ByteBuffer] * 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] * @param count number of 2-bytes registers to read. Buffer size is 2*[count]
*/ */
public fun ModbusDevice.readHoldingRegistersToBuffer(ref: Int, count: Int): ByteBuffer = public fun ModbusDevice.readHoldingRegistersToBuffer(address: Int, count: Int): ByteBuffer =
master.readMultipleRegisters(clientId, ref, count).toBuffer() master.readMultipleRegisters(clientId, address, count).toBuffer()
public fun ModbusDevice.readHoldingRegistersToPacket(ref: Int, count: Int): ByteReadPacket = public fun ModbusDevice.readHoldingRegistersToPacket(address: Int, count: Int): ByteReadPacket =
master.readMultipleRegisters(clientId, ref, count).toPacket() master.readMultipleRegisters(clientId, address, count).toPacket()
public fun ModbusDevice.readDoubleRegister(ref: Int): Double = public fun ModbusDevice.readDoubleRegister(address: Int): Double =
readHoldingRegistersToBuffer(ref, Double.SIZE_BYTES).getDouble() readHoldingRegistersToBuffer(address, Double.SIZE_BYTES).getDouble()
public fun ModbusDevice.readHoldingRegister(ref: Int): Short = public fun ModbusDevice.readHoldingRegister(address: Int): Short =
readHoldingRegisters(ref, 1).first().toShort() 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( master.writeMultipleRegisters(
clientId, clientId,
ref, address,
Array<Register>(values.size) { SimpleRegister().apply { setValue(values[it]) } } Array<Register>(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( master.writeSingleRegister(
clientId, clientId,
ref, address,
SimpleRegister().apply { setValue(value) } 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) } 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) { public fun ModbusDevice.writeShortRegister(address: Int, value: Short) {
master.writeSingleRegister(ref, SimpleRegister().apply { setValue(value) }) master.writeSingleRegister(address, SimpleInputRegister(value.toInt()))
} }
public fun ModbusDevice.modbusRegister( public fun ModbusDevice.modbusRegister(
ref: Int, address: Int,
): ReadWriteProperty<ModbusDevice, Short> = object : ReadWriteProperty<ModbusDevice, Short> { ): ReadWriteProperty<ModbusDevice, Short> = object : ReadWriteProperty<ModbusDevice, Short> {
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) { override fun setValue(thisRef: ModbusDevice, property: KProperty<*>, value: Short) {
writeHoldingRegister(ref, value) writeHoldingRegister(address, value)
} }
} }
public fun ModbusDevice.modbusDoubleRegister( public fun ModbusDevice.modbusDoubleRegister(
ref: Int, address: Int,
): ReadWriteProperty<ModbusDevice, Double> = object : ReadWriteProperty<ModbusDevice, Double> { ): ReadWriteProperty<ModbusDevice, Double> = object : ReadWriteProperty<ModbusDevice, Double> {
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) { override fun setValue(thisRef: ModbusDevice, property: KProperty<*>, value: Double) {
val buffer = ByteBuffer.allocate(Double.SIZE_BYTES).apply { putDouble(value) } val buffer = ByteBuffer.allocate(Double.SIZE_BYTES).apply { putDouble(value) }
writeHoldingRegisters(ref, buffer) writeHoldingRegisters(address, buffer)
} }
} }

View File

@ -17,8 +17,21 @@ public open class ModbusDeviceBySpec<D: Device>(
spec: DeviceSpec<D>, spec: DeviceSpec<D>,
override val clientId: Int, override val clientId: Int,
override val master: AbstractModbusMaster, override val master: AbstractModbusMaster,
private val disposeMasterOnClose: Boolean = true,
meta: Meta = Meta.EMPTY, meta: Meta = Meta.EMPTY,
) : ModbusDevice, DeviceBySpec<D>(spec, context, meta) ) : ModbusDevice, DeviceBySpec<D>(spec, context, meta){
override suspend fun open() {
master.connect()
super<ModbusDevice>.open()
}
override fun close() {
if(disposeMasterOnClose){
master.disconnect()
}
super<ModbusDevice>.close()
}
}
public class ModbusHub( public class ModbusHub(

View File

@ -7,63 +7,48 @@ public sealed class ModbusRegistryKey {
public abstract val address: Int public abstract val address: Int
public open val count: Int = 1 public open val count: Int = 1
/** /**
* Read-only boolean value * Read-only boolean value
*/ */
public data class Coil(override val address: Int) : ModbusRegistryKey() { public data class Coil(override val address: Int) : ModbusRegistryKey()
init {
require(address in 1..9999) { "Coil address must be in 1..9999 range" }
}
}
/** /**
* Read-write boolean value * Read-write boolean value
*/ */
public data class DiscreteInput(override val address: Int) : ModbusRegistryKey() { public data class DiscreteInput(override val address: Int) : ModbusRegistryKey()
init {
require(address in 10001..19999) { "DiscreteInput address must be in 10001..19999 range" }
}
}
/** /**
* Read-only binary value * Read-only binary value
*/ */
public data class InputRegister(override val address: Int) : ModbusRegistryKey() { public open class InputRegister(override val address: Int) : ModbusRegistryKey() {
init { override fun toString(): String = "InputRegister(address=$address)"
require(address in 20001..29999) { "InputRegister address must be in 20001..29999 range" }
}
} }
public data class InputRange<T>( public class InputRange<T>(
override val address: Int, address: Int,
override val count: Int, override val count: Int,
public val format: IOFormat<T>, public val format: IOFormat<T>,
) : ModbusRegistryKey() { ) : InputRegister(address) {
public val endAddress: Int get() = address + count 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() { public open class HoldingRegister(override val address: Int) : ModbusRegistryKey() {
init { override fun toString(): String = "HoldingRegister(address=$address)"
require(address in 30001..39999) { "HoldingRegister address must be in 30001..39999 range" }
}
} }
public data class HoldingRange<T>( public class HoldingRange<T>(
override val address: Int, address: Int,
override val count: Int, override val count: Int,
public val format: IOFormat<T>, public val format: IOFormat<T>,
) : ModbusRegistryKey() { ) : HoldingRegister(address) {
public val endAddress: Int get() = address + count 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 = protected fun coil(address: Int, description: String = ""): ModbusRegistryKey.Coil =
register(ModbusRegistryKey.Coil(address), description) 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 = protected fun discrete(address: Int, description: String = ""): ModbusRegistryKey.DiscreteInput =
register(ModbusRegistryKey.DiscreteInput(address), description) 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 = protected fun input(address: Int, description: String = ""): ModbusRegistryKey.InputRegister =
register(ModbusRegistryKey.InputRegister(address), description) register(ModbusRegistryKey.InputRegister(address), description)
@ -101,17 +81,6 @@ public abstract class ModbusRegistryMap {
): ModbusRegistryKey.InputRange<T> = ): ModbusRegistryKey.InputRange<T> =
register(ModbusRegistryKey.InputRange(address, count, reader), description) register(ModbusRegistryKey.InputRange(address, count, reader), description)
protected fun inputByOffset(offset: Int, description: String = ""): ModbusRegistryKey.InputRegister =
register(ModbusRegistryKey.InputRegister(20000 + offset), description)
protected fun <T> inputByOffset(
offset: Int,
count: Int,
reader: IOFormat<T>,
description: String = "",
): ModbusRegistryKey.InputRange<T> =
register(ModbusRegistryKey.InputRange(20000 + offset, count, reader), description)
protected fun register(address: Int, description: String = ""): ModbusRegistryKey.HoldingRegister = protected fun register(address: Int, description: String = ""): ModbusRegistryKey.HoldingRegister =
register(ModbusRegistryKey.HoldingRegister(address), description) register(ModbusRegistryKey.HoldingRegister(address), description)
@ -123,32 +92,62 @@ public abstract class ModbusRegistryMap {
): ModbusRegistryKey.HoldingRange<T> = ): ModbusRegistryKey.HoldingRange<T> =
register(ModbusRegistryKey.HoldingRange(address, count, format), description) register(ModbusRegistryKey.HoldingRange(address, count, format), description)
protected fun registerByOffset(offset: Int, description: String = ""): ModbusRegistryKey.HoldingRegister =
register(ModbusRegistryKey.HoldingRegister(30000 + offset), description)
protected fun <T> registerByOffset(
offset: Int,
count: Int,
format: IOFormat<T>,
description: String = "",
): ModbusRegistryKey.HoldingRange<T> =
register(ModbusRegistryKey.HoldingRange(offset + 30000, count, format), description)
public companion object { public companion object {
public fun validate(map: ModbusRegistryMap) { public fun validate(map: ModbusRegistryMap) {
map.entries.keys.sortedBy { it.address }.zipWithNext().forEach { (l, r) -> var lastCoil: ModbusRegistryKey.Coil? = null
if (l.address + l.count > r.address) error("Key $l overlaps with key $r") 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) { public fun print(map: ModbusRegistryMap, to: Appendable = System.out) {
validate(map) validate(map)
map.entries.entries.sortedBy { it.key.address }.forEach { (key, description) -> map.entries.entries
.sortedWith(
Comparator.comparingInt<Map.Entry<ModbusRegistryKey, String>?> { it.key.sectionNumber }
.thenComparingInt { it.key.address }
)
.forEach { (key, description) ->
val typeString = when (key) { val typeString = when (key) {
is ModbusRegistryKey.Coil -> "Coil" is ModbusRegistryKey.Coil -> "Coil"
is ModbusRegistryKey.DiscreteInput -> "Discrete" is ModbusRegistryKey.DiscreteInput -> "Discrete"
is ModbusRegistryKey.HoldingRange<*>, is ModbusRegistryKey.HoldingRegister -> "Register" is ModbusRegistryKey.HoldingRegister -> "Register"
is ModbusRegistryKey.InputRange<*>, is ModbusRegistryKey.InputRegister -> "Input" is ModbusRegistryKey.InputRegister -> "Input"
} }
val rangeString = if (key.count == 1) { val rangeString = if (key.count == 1) {
key.address.toString() key.address.toString()