243 lines
6.8 KiB
Kotlin

package space.kscience.controls.modbus
import com.ghgande.j2mod.modbus.procimg.*
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.io.Buffer
import space.kscience.controls.api.Device
import space.kscience.controls.ports.readShort
import space.kscience.controls.spec.*
import space.kscience.dataforge.io.Binary
public class DeviceProcessImageBuilder<D : Device> internal constructor(
private val device: D,
public val image: ProcessImageImplementation,
) {
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: MutableDevicePropertySpec<D, Boolean>,
): ObservableDigitalOut = bind(key) { coil ->
coil.addObserver { _, _ ->
device.writeAsync(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: MutableDevicePropertySpec<D, Short>,
): ObservableRegister = bind(key) { register ->
register.addObserver { _, _ ->
device.writeAsync(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 binary = Binary {
key.format.writeTo(this, value)
}
registers.forEachIndexed { index, register ->
register.setValue(binary.readShort(index * 2))
}
}
}
/**
* Trigger [block] if one of register changes.
*/
private fun List<ObservableRegister>.onChange(block: suspend (Buffer) -> Unit) {
var ready = false
forEach { register ->
register.addObserver { _, _ ->
ready = true
}
}
device.launch {
val builder = Buffer()
while (isActive) {
delay(1)
if (ready) {
val packet = builder.apply {
forEach { value ->
writeShort(value.toShort())
}
}
block(packet)
ready = false
}
}
}
}
public fun <T> bind(key: ModbusRegistryKey.HoldingRange<T>, propertySpec: MutableDevicePropertySpec<D, T>) {
val registers = List(key.count) {
ObservableRegister()
}
registers.forEachIndexed { index, register ->
image.addRegister(key.address + index, register)
}
registers.onChange { packet ->
device.write(propertySpec, key.format.readFrom(packet))
}
device.useProperty(propertySpec) { value ->
val binary = Binary {
key.format.writeTo(this, value)
}
registers.forEachIndexed { index, observableRegister ->
observableRegister.setValue(binary.readShort(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 ->
image.addRegister(key.address + index, register)
}
registers.onChange { packet ->
device.launch {
device.action(key.format.readFrom(packet))
}
}
return registers
}
}
/**
* Bind the device to Modbus slave (server) image.
*/
public fun <D : Device> D.bindProcessImage(
unitId: Int = 0,
openOnBind: Boolean = true,
binding: DeviceProcessImageBuilder<D>.() -> Unit,
): ProcessImage {
val image = SimpleProcessImage(unitId)
DeviceProcessImageBuilder(this, image).apply(binding)
image.setLocked(true)
if (openOnBind) {
launch {
start()
}
}
return image
}