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 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"
}

View File

@ -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 <D : Device, I, O> DeviceActionSpec<D, I, O>.executeWithMeta
* A base abstractions for [Device], introducing specifications for properties
*/
public abstract class DeviceBase<D : Device>(
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<D : Device>(
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<D : Device>(
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.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<D : Device>(
public val spec: DeviceSpec<in D>,
context: Context = Global,
context: Context,
meta: Meta = Meta.EMPTY,
) : DeviceBase<D>(context, meta) {
override val properties: Map<String, DevicePropertySpec<D, *>> get() = spec.properties
@ -26,4 +25,6 @@ public open class DeviceBySpec<D : Device>(
self.onClose()
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.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<D : Device> private constructor(
private val mapping: Map<DevicePropertySpec<D, *>, ModbusRegistryKey>,
) : Map<DevicePropertySpec<D, *>, ModbusRegistryKey> by mapping {
public class Builder<D : Device> {
private val mapping = HashMap<DevicePropertySpec<D, *>, ModbusRegistryKey>()
public class DeviceProcessImageBuilder<D : Device>(
private val device: D,
public val image: ProcessImageImplementation,
) {
public fun bind(propertySpec: DevicePropertySpec<D, Boolean>, key: ModbusRegistryKey.DiscreteInput) {
mapping[propertySpec] = key
}
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 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<D, Boolean>,
): 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<D, Boolean>,
): 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<D, Short>,
): 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<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) {
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 <T> bindAction(
key: ModbusRegistryKey.HoldingRange<T>,
action: suspend D.(T) -> Unit,
): List<ObservableRegister> {
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 <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 {
public fun <D : Device> D.bindProcessImage(
openOnBind: Boolean = true,
binding: DeviceProcessImageBuilder<D>.() -> Unit,
): ProcessImage {
val image = SimpleProcessImage()
mapping.forEach { (spec, key) ->
when (key) {
is ModbusRegistryKey.Coil -> {
spec as WritableDevicePropertySpec<D, Boolean>
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<D, Boolean>
val input = SimpleDigitalIn()
image.setDigitalIn(key.address, input)
useProperty(spec) { value ->
input.set(value)
}
}
is ModbusRegistryKey.HoldingRegister -> {
spec as WritableDevicePropertySpec<D, Short>
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<D, Short>
val input = SimpleInputRegister()
image.setRegister(key.address, input)
useProperty(spec) { value ->
input.setValue(value)
}
}
is ModbusRegistryKey.HoldingRange<*> -> {
spec as WritableDevicePropertySpec<D, Any?>
key as ModbusRegistryKey.HoldingRange<Any?>
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<D, Any?>
key as ModbusRegistryKey.InputRange<Any?>
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 : 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.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<InputRegister> =
master.readInputRegisters(clientId, ref, count).toList()
public fun ModbusDevice.readInputRegisters(address: Int, count: Int): List<InputRegister> =
master.readInputRegisters(clientId, address, count).toList()
private fun Array<out InputRegister>.toBuffer(): ByteBuffer {
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 =
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<Register> =
master.readMultipleRegisters(clientId, ref, count).toList()
public fun ModbusDevice.readHoldingRegisters(address: Int, count: Int): List<Register> =
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<Register>(values.size) { SimpleRegister().apply { setValue(values[it]) } }
address,
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(
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<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) {
writeHoldingRegister(ref, value)
writeHoldingRegister(address, value)
}
}
public fun ModbusDevice.modbusDoubleRegister(
ref: Int,
address: Int,
): 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) {
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>,
override val clientId: Int,
override val master: AbstractModbusMaster,
private val disposeMasterOnClose: Boolean = true,
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(

View File

@ -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<T>(
override val address: Int,
public class InputRange<T>(
address: Int,
override val count: Int,
public val format: IOFormat<T>,
) : 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<T>(
override val address: Int,
public class HoldingRange<T>(
address: Int,
override val count: Int,
public val format: IOFormat<T>,
) : 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<T> =
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 =
register(ModbusRegistryKey.HoldingRegister(address), description)
@ -123,40 +92,70 @@ public abstract class ModbusRegistryMap {
): ModbusRegistryKey.HoldingRange<T> =
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 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<Map.Entry<ModbusRegistryKey, String>?> { 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")
}
}
}
}