Compare commits

..

No commits in common. "1619fdadf23e033b049d7ad3b724f8acffa012bb" and "290010fc8c2b71f789f8aa990080b8502b7325d8" have entirely different histories.

30 changed files with 114 additions and 488 deletions

View File

@ -1,3 +1,4 @@
import space.kscience.gradle.isInDevelopment
import space.kscience.gradle.useApache2Licence import space.kscience.gradle.useApache2Licence
import space.kscience.gradle.useSPCTeam import space.kscience.gradle.useSPCTeam
@ -13,18 +14,25 @@ val xodusVersion by extra("2.0.1")
allprojects { allprojects {
group = "space.kscience" group = "space.kscience"
version = "0.3.0-dev-1" version = "0.2.2-dev-2"
repositories{ repositories{
maven("https://maven.pkg.jetbrains.space/spc/p/sci/dev") maven("https://maven.pkg.jetbrains.space/spc/p/sci/dev")
} }
} }
ksciencePublish { ksciencePublish {
pom("https://github.com/SciProgCentre/controls-kt") { pom("https://github.com/SciProgCentre/controls.kt") {
useApache2Licence() useApache2Licence()
useSPCTeam() useSPCTeam()
} }
repository("spc","https://maven.sciprog.center/kscience") github("controls.kt", "SciProgCentre")
space(
if (isInDevelopment) {
"https://maven.pkg.jetbrains.space/spc/p/sci/dev"
} else {
"https://maven.pkg.jetbrains.space/spc/p/sci/maven"
}
)
sonatype("https://oss.sonatype.org") sonatype("https://oss.sonatype.org")
} }

View File

@ -1,20 +0,0 @@
plugins {
id("space.kscience.gradle.mpp")
`maven-publish`
}
description = """
A low-code constructor foe composite devices simulation
""".trimIndent()
kscience{
jvm()
js()
dependencies {
api(projects.controlsCore)
}
}
readme{
maturity = space.kscience.gradle.Maturity.PROTOTYPE
}

View File

@ -1,51 +0,0 @@
package center.sciprog.controls.devices.misc
import kotlinx.coroutines.Job
import space.kscience.controls.api.Device
import space.kscience.controls.spec.DeviceBySpec
import space.kscience.controls.spec.DevicePropertySpec
import space.kscience.controls.spec.DeviceSpec
import space.kscience.controls.spec.doubleProperty
import space.kscience.dataforge.context.Context
import space.kscience.dataforge.meta.transformations.MetaConverter
/**
* A single axis drive
*/
public interface Drive : Device {
/**
* Get or set target value
*/
public var target: Double
/**
* Current position value
*/
public val position: Double
public companion object : DeviceSpec<Drive>() {
public val target: DevicePropertySpec<Drive, Double> by property(MetaConverter.double, Drive::target)
public val position: DevicePropertySpec<Drive, Double> by doubleProperty { position }
}
}
/**
* Virtual [Drive] with speed limit
*/
public class VirtualDrive(
context: Context,
value: Double,
private val speed: Double,
) : DeviceBySpec<Drive>(Drive, context), Drive {
private var moveJob: Job? = null
override var position: Double = value
private set
override var target: Double = value
}

View File

@ -1,31 +0,0 @@
package center.sciprog.controls.devices.misc
import space.kscience.controls.api.Device
import space.kscience.controls.spec.DeviceBySpec
import space.kscience.controls.spec.DevicePropertySpec
import space.kscience.controls.spec.DeviceSpec
import space.kscience.controls.spec.booleanProperty
import space.kscience.dataforge.context.Context
/**
* A limit switch device
*/
public interface LimitSwitch : Device {
public val locked: Boolean
public companion object : DeviceSpec<LimitSwitch>() {
public val locked: DevicePropertySpec<LimitSwitch, Boolean> by booleanProperty { locked }
}
}
/**
* Virtual [LimitSwitch]
*/
public class VirtualLimitSwitch(
context: Context,
private val lockedFunction: () -> Boolean,
) : DeviceBySpec<LimitSwitch>(LimitSwitch, context), LimitSwitch {
override val locked: Boolean get() = lockedFunction()
}

View File

@ -1,146 +0,0 @@
package center.sciprog.controls.devices.misc
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.datetime.Clock
import kotlinx.datetime.Instant
import space.kscience.controls.api.Device
import space.kscience.controls.spec.DeviceBySpec
import space.kscience.controls.spec.DeviceSpec
import space.kscience.controls.spec.doubleProperty
import space.kscience.dataforge.context.Context
import space.kscience.dataforge.context.Factory
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.double
import space.kscience.dataforge.meta.get
import space.kscience.dataforge.meta.transformations.MetaConverter
import kotlin.math.pow
import kotlin.time.Duration
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.DurationUnit
interface PidRegulator : Device {
/**
* Proportional coefficient
*/
val kp: Double
/**
* Integral coefficient
*/
val ki: Double
/**
* Differential coefficient
*/
val kd: Double
/**
* The target value for PID
*/
var target: Double
/**
* Read current value
*/
suspend fun read(): Double
companion object : DeviceSpec<PidRegulator>() {
val target by property(MetaConverter.double, PidRegulator::target)
val value by doubleProperty { read() }
}
}
/**
*
*/
class VirtualPid(
context: Context,
override val kp: Double,
override val ki: Double,
override val kd: Double,
val mass: Double,
override var target: Double = 0.0,
private val dt: Duration = 0.5.milliseconds,
private val clock: Clock = Clock.System,
) : DeviceBySpec<PidRegulator>(PidRegulator, context), PidRegulator {
private val mutex = Mutex()
private var lastTime: Instant = clock.now()
private var lastValue: Double = target
private var value: Double = target
private var velocity: Double = 0.0
private var acceleration: Double = 0.0
private var integral: Double = 0.0
private var updateJob: Job? = null
override suspend fun onStart() {
updateJob = launch {
while (isActive) {
delay(dt)
mutex.withLock {
val realTime = clock.now()
val delta = target - value
val dtSeconds = (realTime - lastTime).toDouble(DurationUnit.SECONDS)
integral += delta * dtSeconds
val derivative = (value - lastValue) / dtSeconds
//set last time and value to new values
lastTime = realTime
lastValue = value
// compute new value based on velocity and acceleration from the previous step
value += velocity * dtSeconds + acceleration * dtSeconds.pow(2) / 2
// compute new velocity based on acceleration on the previous step
velocity += acceleration * dtSeconds
//compute force for the next step based on current values
acceleration = (kp * delta + ki * integral + kd * derivative) / mass
check(value.isFinite() && velocity.isFinite()) {
"Value $value is not finite"
}
}
}
}
}
override fun onStop() {
updateJob?.cancel()
super<PidRegulator>.stop()
}
override suspend fun read(): Double = value
suspend fun readVelocity(): Double = velocity
suspend fun readAcceleration(): Double = acceleration
suspend fun write(newTarget: Double) = mutex.withLock {
require(newTarget.isFinite()) { "Value $newTarget is not valid" }
target = newTarget
}
companion object : Factory<Device> {
override fun build(context: Context, meta: Meta) = VirtualPid(
context,
meta["kp"].double ?: error("Kp is not defined"),
meta["ki"].double ?: error("Ki is not defined"),
meta["kd"].double ?: error("Kd is not defined"),
meta["m"].double ?: error("Mass is not defined"),
)
}
}

View File

@ -20,19 +20,8 @@ import space.kscience.dataforge.names.Name
* A lifecycle state of a device * A lifecycle state of a device
*/ */
public enum class DeviceLifecycleState{ public enum class DeviceLifecycleState{
/**
* Device is initializing
*/
INIT, INIT,
/**
* The Device is initialized and running
*/
OPEN, OPEN,
/**
* The Device is closed
*/
CLOSED CLOSED
} }
@ -42,14 +31,13 @@ public enum class DeviceLifecycleState{
* When canceled, cancels all running processes. * When canceled, cancels all running processes.
*/ */
@Type(DEVICE_TARGET) @Type(DEVICE_TARGET)
public interface Device : ContextAware, CoroutineScope { public interface Device : AutoCloseable, ContextAware, CoroutineScope {
/** /**
* Initial configuration meta for the device * Initial configuration meta for the device
*/ */
public val meta: Meta get() = Meta.EMPTY public val meta: Meta get() = Meta.EMPTY
/** /**
* List of supported property descriptors * List of supported property descriptors
*/ */
@ -99,12 +87,12 @@ public interface Device : ContextAware, CoroutineScope {
/** /**
* Initialize the device. This function suspends until the device is finished initialization * Initialize the device. This function suspends until the device is finished initialization
*/ */
public suspend fun start(): Unit = Unit public suspend fun open(): Unit = Unit
/** /**
* Close and terminate the device. This function does not wait for the device to be closed. * Close and terminate the device. This function does not wait for the device to be closed.
*/ */
public fun stop() { override fun close() {
logger.info { "Device $this is closed" } logger.info { "Device $this is closed" }
cancel("The device is closed") cancel("The device is closed")
} }

View File

@ -25,7 +25,7 @@ public sealed class DeviceMessage {
public abstract val time: Instant? public abstract val time: Instant?
/** /**
* Update the source device name for composition. If the original name is null, the resulting name is also null. * Update the source device name for composition. If the original name is null, resulting name is also null.
*/ */
public abstract fun changeSource(block: (Name) -> Name): DeviceMessage public abstract fun changeSource(block: (Name) -> Name): DeviceMessage
@ -203,12 +203,12 @@ public data class EmptyDeviceMessage(
public data class DeviceLogMessage( public data class DeviceLogMessage(
val message: String, val message: String,
val data: Meta? = null, val data: Meta? = null,
override val sourceDevice: Name = Name.EMPTY, override val sourceDevice: Name? = null,
override val targetDevice: Name? = null, override val targetDevice: Name? = null,
override val comment: String? = null, override val comment: String? = null,
@EncodeDefault override val time: Instant? = Clock.System.now(), @EncodeDefault override val time: Instant? = Clock.System.now(),
) : DeviceMessage() { ) : DeviceMessage() {
override fun changeSource(block: (Name) -> Name): DeviceMessage = copy(sourceDevice = block(sourceDevice)) override fun changeSource(block: (Name) -> Name): DeviceMessage = copy(sourceDevice = sourceDevice?.let(block))
} }
/** /**
@ -220,7 +220,7 @@ public data class DeviceErrorMessage(
public val errorMessage: String?, public val errorMessage: String?,
public val errorType: String? = null, public val errorType: String? = null,
public val errorStackTrace: String? = null, public val errorStackTrace: String? = null,
override val sourceDevice: Name = Name.EMPTY, override val sourceDevice: Name,
override val targetDevice: Name? = null, override val targetDevice: Name? = null,
override val comment: String? = null, override val comment: String? = null,
@EncodeDefault override val time: Instant? = Clock.System.now(), @EncodeDefault override val time: Instant? = Clock.System.now(),

View File

@ -12,10 +12,10 @@ import space.kscience.dataforge.meta.descriptors.MetaDescriptorBuilder
@Serializable @Serializable
public class PropertyDescriptor( public class PropertyDescriptor(
public val name: String, public val name: String,
public var description: String? = null, public var info: String? = null,
public var metaDescriptor: MetaDescriptor = MetaDescriptor(), public var metaDescriptor: MetaDescriptor = MetaDescriptor(),
public var readable: Boolean = true, public var readable: Boolean = true,
public var mutable: Boolean = false public var writable: Boolean = false
) )
public fun PropertyDescriptor.metaDescriptor(block: MetaDescriptorBuilder.()->Unit){ public fun PropertyDescriptor.metaDescriptor(block: MetaDescriptorBuilder.()->Unit){

View File

@ -40,7 +40,7 @@ public class DeviceManager : AbstractPlugin(), DeviceHub {
public fun <D : Device> DeviceManager.install(name: String, device: D): D { public fun <D : Device> DeviceManager.install(name: String, device: D): D {
registerDevice(NameToken(name), device) registerDevice(NameToken(name), device)
device.launch { device.launch {
device.start() device.open()
} }
return device return device
} }

View File

@ -5,7 +5,6 @@ import io.ktor.utils.io.core.readBytes
import io.ktor.utils.io.core.reset import io.ktor.utils.io.core.reset
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onCompletion
import kotlinx.coroutines.flow.transform import kotlinx.coroutines.flow.transform
/** /**
@ -17,10 +16,6 @@ public fun Flow<ByteArray>.withDelimiter(delimiter: ByteArray): Flow<ByteArray>
val output = BytePacketBuilder() val output = BytePacketBuilder()
var matcherPosition = 0 var matcherPosition = 0
onCompletion {
output.close()
}
return transform { chunk -> return transform { chunk ->
chunk.forEach { byte -> chunk.forEach { byte ->
output.writeByte(byte) output.writeByte(byte)

View File

@ -1,9 +1,12 @@
package space.kscience.controls.spec package space.kscience.controls.spec
import kotlinx.coroutines.* import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.newCoroutineContext
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.*
@ -66,28 +69,8 @@ public abstract class DeviceBase<D : Device>(
override val actionDescriptors: Collection<ActionDescriptor> override val actionDescriptors: Collection<ActionDescriptor>
get() = actions.values.map { it.descriptor } get() = actions.values.map { it.descriptor }
private val sharedMessageFlow: MutableSharedFlow<DeviceMessage> = MutableSharedFlow(
replay = meta["message.buffer"].int ?: 1000,
onBufferOverflow = BufferOverflow.DROP_OLDEST
)
override val coroutineContext: CoroutineContext by lazy { override val coroutineContext: CoroutineContext by lazy {
context.newCoroutineContext( context.newCoroutineContext(SupervisorJob(context.coroutineContext[Job]) + CoroutineName("Device $this"))
SupervisorJob(context.coroutineContext[Job]) +
CoroutineName("Device $this") +
CoroutineExceptionHandler { _, throwable ->
launch {
sharedMessageFlow.emit(
DeviceErrorMessage(
errorMessage = throwable.message,
errorType = throwable::class.simpleName,
errorStackTrace = throwable.stackTraceToString()
)
)
}
}
)
} }
@ -96,6 +79,11 @@ public abstract class DeviceBase<D : Device>(
*/ */
private val logicalState: HashMap<String, Meta?> = HashMap() private val logicalState: HashMap<String, Meta?> = HashMap()
private val sharedMessageFlow: MutableSharedFlow<DeviceMessage> = MutableSharedFlow(
replay = meta["message.buffer"].int ?: 1000,
onBufferOverflow = BufferOverflow.DROP_OLDEST
)
public override val messageFlow: SharedFlow<DeviceMessage> get() = sharedMessageFlow public override val messageFlow: SharedFlow<DeviceMessage> get() = sharedMessageFlow
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
@ -192,30 +180,18 @@ public abstract class DeviceBase<D : Device>(
override var lifecycleState: DeviceLifecycleState = DeviceLifecycleState.INIT override var lifecycleState: DeviceLifecycleState = DeviceLifecycleState.INIT
protected set protected set
protected open suspend fun onStart() {
}
@OptIn(DFExperimental::class) @OptIn(DFExperimental::class)
final override suspend fun start() { override suspend fun open() {
super.start() super.open()
lifecycleState = DeviceLifecycleState.INIT
onStart()
lifecycleState = DeviceLifecycleState.OPEN lifecycleState = DeviceLifecycleState.OPEN
} }
protected open fun onStop() {
}
@OptIn(DFExperimental::class) @OptIn(DFExperimental::class)
final override fun stop() { override fun close() {
onStop()
lifecycleState = DeviceLifecycleState.CLOSED lifecycleState = DeviceLifecycleState.CLOSED
super.stop() super.close()
} }
abstract override fun toString(): String abstract override fun toString(): String
} }

View File

@ -16,14 +16,15 @@ public open class DeviceBySpec<D : Device>(
override val properties: Map<String, DevicePropertySpec<D, *>> get() = spec.properties override val properties: Map<String, DevicePropertySpec<D, *>> get() = spec.properties
override val actions: Map<String, DeviceActionSpec<D, *, *>> get() = spec.actions override val actions: Map<String, DeviceActionSpec<D, *, *>> get() = spec.actions
override suspend fun onStart(): Unit = with(spec) { override suspend fun open(): Unit = with(spec) {
super.open()
self.onOpen() self.onOpen()
} }
override fun onStop(): Unit = with(spec){ override fun close(): Unit = with(spec) {
self.onClose() self.onClose()
super.close()
} }
override fun toString(): String = "Device(spec=$spec)" override fun toString(): String = "Device(spec=$spec)"
} }

View File

@ -20,7 +20,7 @@ public annotation class InternalDeviceAPI
/** /**
* Specification for a device read-only property * Specification for a device read-only property
*/ */
public interface DevicePropertySpec<in D, T> { public interface DevicePropertySpec<in D : Device, T> {
/** /**
* Property descriptor * Property descriptor
*/ */
@ -53,7 +53,7 @@ public interface WritableDevicePropertySpec<in D : Device, T> : DevicePropertySp
} }
public interface DeviceActionSpec<in D, I, O> { public interface DeviceActionSpec<in D : Device, I, O> {
/** /**
* Action descriptor * Action descriptor
*/ */

View File

@ -53,7 +53,7 @@ public abstract class DeviceSpec<D : Device> {
val deviceProperty = object : DevicePropertySpec<D, T> { val deviceProperty = object : DevicePropertySpec<D, T> {
override val descriptor: PropertyDescriptor = PropertyDescriptor(property.name).apply { override val descriptor: PropertyDescriptor = PropertyDescriptor(property.name).apply {
//TODO add type from converter //TODO add type from converter
mutable = true writable = true
}.apply(descriptorBuilder) }.apply(descriptorBuilder)
override val converter: MetaConverter<T> = converter override val converter: MetaConverter<T> = converter
@ -78,7 +78,7 @@ public abstract class DeviceSpec<D : Device> {
override val descriptor: PropertyDescriptor = PropertyDescriptor(property.name).apply { override val descriptor: PropertyDescriptor = PropertyDescriptor(property.name).apply {
//TODO add the type from converter //TODO add the type from converter
mutable = true writable = true
}.apply(descriptorBuilder) }.apply(descriptorBuilder)
override val converter: MetaConverter<T> = converter override val converter: MetaConverter<T> = converter
@ -127,7 +127,7 @@ public abstract class DeviceSpec<D : Device> {
PropertyDelegateProvider { _: DeviceSpec<D>, property: KProperty<*> -> PropertyDelegateProvider { _: DeviceSpec<D>, property: KProperty<*> ->
val propertyName = name ?: property.name val propertyName = name ?: property.name
val deviceProperty = object : WritableDevicePropertySpec<D, T> { val deviceProperty = object : WritableDevicePropertySpec<D, T> {
override val descriptor: PropertyDescriptor = PropertyDescriptor(propertyName, mutable = true) override val descriptor: PropertyDescriptor = PropertyDescriptor(propertyName, writable = true)
.apply(descriptorBuilder) .apply(descriptorBuilder)
override val converter: MetaConverter<T> = converter override val converter: MetaConverter<T> = converter
@ -224,7 +224,7 @@ public fun <T, D : DeviceBase<D>> DeviceSpec<D>.logicalProperty(
val propertyName = name ?: property.name val propertyName = name ?: property.name
override val descriptor: PropertyDescriptor = PropertyDescriptor(propertyName).apply { override val descriptor: PropertyDescriptor = PropertyDescriptor(propertyName).apply {
//TODO add type from converter //TODO add type from converter
mutable = true writable = true
}.apply(descriptorBuilder) }.apply(descriptorBuilder)
override val converter: MetaConverter<T> = converter override val converter: MetaConverter<T> = converter

View File

@ -1,36 +0,0 @@
package space.kscience.controls.spec
import space.kscience.controls.api.Device
import space.kscience.controls.api.DeviceHub
import space.kscience.controls.manager.DeviceManager
import space.kscience.dataforge.context.Factory
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.get
import space.kscience.dataforge.names.NameToken
import kotlin.collections.Map
import kotlin.collections.component1
import kotlin.collections.component2
import kotlin.collections.mapValues
import kotlin.collections.mutableMapOf
import kotlin.collections.set
public class DeviceTree(
public val deviceManager: DeviceManager,
public val meta: Meta,
builder: Builder,
) : DeviceHub {
public class Builder(public val manager: DeviceManager) {
internal val childrenFactories = mutableMapOf<NameToken, Factory<Device>>()
public fun <D : Device> device(name: String, factory: Factory<Device>) {
childrenFactories[NameToken.parse(name)] = factory
}
}
override val devices: Map<NameToken, Device> = builder.childrenFactories.mapValues { (token, factory) ->
val devicesMeta = meta["devices"]
factory.build(deviceManager.context, devicesMeta?.get(token) ?: Meta.EMPTY)
}
}

View File

@ -12,7 +12,7 @@ description = """
dependencies { dependencies {
api(projects.controlsCore) api(projects.controlsCore)
api("com.ghgande:j2mod:3.2.0") api("com.ghgande:j2mod:3.1.1")
} }
readme{ readme{

View File

@ -237,7 +237,7 @@ public fun <D : Device> D.bindProcessImage(
image.setLocked(true) image.setLocked(true)
if (openOnBind) { if (openOnBind) {
launch { launch {
start() open()
} }
} }
return image return image

View File

@ -20,14 +20,16 @@ public open class ModbusDeviceBySpec<D: Device>(
private val disposeMasterOnClose: Boolean = true, 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 onStart() { override suspend fun open() {
master.connect() master.connect()
super<DeviceBySpec>.open()
} }
override fun onStop() { override fun close() {
if(disposeMasterOnClose){ if(disposeMasterOnClose){
master.disconnect() master.disconnect()
} }
super<ModbusDevice>.close()
} }
} }

View File

@ -1,15 +1,8 @@
package space.kscience.controls.modbus package space.kscience.controls.modbus
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.buildJsonArray
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.put
import space.kscience.dataforge.io.IOFormat import space.kscience.dataforge.io.IOFormat
/**
* Modbus registry key
*/
public sealed class ModbusRegistryKey { 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
@ -32,9 +25,6 @@ public sealed class ModbusRegistryKey {
override fun toString(): String = "InputRegister(address=$address)" override fun toString(): String = "InputRegister(address=$address)"
} }
/**
* A range of read-only register encoding a single value
*/
public class InputRange<T>( public class InputRange<T>(
address: Int, address: Int,
override val count: Int, override val count: Int,
@ -46,16 +36,10 @@ public sealed class ModbusRegistryKey {
} }
/**
* A single read-write register
*/
public open class HoldingRegister(override val address: Int) : ModbusRegistryKey() { public open class HoldingRegister(override val address: Int) : ModbusRegistryKey() {
override fun toString(): String = "HoldingRegister(address=$address)" override fun toString(): String = "HoldingRegister(address=$address)"
} }
/**
* A range of read-write registers encoding a single value
*/
public class HoldingRange<T>( public class HoldingRange<T>(
address: Int, address: Int,
override val count: Int, override val count: Int,
@ -68,9 +52,6 @@ public sealed class ModbusRegistryKey {
} }
} }
/**
* A base class for modbus registers
*/
public abstract class ModbusRegistryMap { public abstract class ModbusRegistryMap {
private val _entries: MutableMap<ModbusRegistryKey, String> = mutableMapOf<ModbusRegistryKey, String>() private val _entries: MutableMap<ModbusRegistryKey, String> = mutableMapOf<ModbusRegistryKey, String>()
@ -82,56 +63,36 @@ public abstract class ModbusRegistryMap {
return key return key
} }
/**
* Register a [ModbusRegistryKey.Coil] key and return it
*/
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)
/**
* Register a [ModbusRegistryKey.DiscreteInput] key and return it
*/
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)
/**
* Register a [ModbusRegistryKey.InputRegister] key and return it
*/
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)
/**
* Register a [ModbusRegistryKey.InputRange] key and return it
*/
protected fun <T> input( protected fun <T> input(
address: Int, address: Int,
count: Int, count: Int,
reader: IOFormat<T>, reader: IOFormat<T>,
description: String = "", description: String = "",
): ModbusRegistryKey.InputRange<T> = register(ModbusRegistryKey.InputRange(address, count, reader), description) ): ModbusRegistryKey.InputRange<T> =
register(ModbusRegistryKey.InputRange(address, count, reader), description)
/**
* Register a [ModbusRegistryKey.HoldingRegister] key and return it
*/
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)
/**
* Register a [ModbusRegistryKey.HoldingRange] key and return it
*/
protected fun <T> register( protected fun <T> register(
address: Int, address: Int,
count: Int, count: Int,
format: IOFormat<T>, format: IOFormat<T>,
description: String = "", description: String = "",
): ModbusRegistryKey.HoldingRange<T> = register(ModbusRegistryKey.HoldingRange(address, count, format), description) ): ModbusRegistryKey.HoldingRange<T> =
register(ModbusRegistryKey.HoldingRange(address, count, format), description)
public companion object { public companion object {
/**
* Validate the register map. Throw an error if the map is invalid
*/
public fun validate(map: ModbusRegistryMap) { public fun validate(map: ModbusRegistryMap) {
var lastCoil: ModbusRegistryKey.Coil? = null var lastCoil: ModbusRegistryKey.Coil? = null
var lastDiscreteInput: ModbusRegistryKey.DiscreteInput? = null var lastDiscreteInput: ModbusRegistryKey.DiscreteInput? = null
@ -166,62 +127,36 @@ public abstract class ModbusRegistryMap {
} }
} }
} private val ModbusRegistryKey.sectionNumber
} get() = when (this) {
is ModbusRegistryKey.Coil -> 1
private val ModbusRegistryKey.sectionNumber is ModbusRegistryKey.DiscreteInput -> 2
get() = when (this) { is ModbusRegistryKey.HoldingRegister -> 4
is ModbusRegistryKey.Coil -> 1 is ModbusRegistryKey.InputRegister -> 3
is ModbusRegistryKey.DiscreteInput -> 2
is ModbusRegistryKey.HoldingRegister -> 4
is ModbusRegistryKey.InputRegister -> 3
}
public fun ModbusRegistryMap.print(to: Appendable = System.out) {
ModbusRegistryMap.validate(this)
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 - 1}"
}
to.appendLine("${typeString}\t$rangeString\t$description")
}
}
public fun ModbusRegistryMap.toJson(): JsonArray = buildJsonArray { public fun print(map: ModbusRegistryMap, to: Appendable = System.out) {
ModbusRegistryMap.validate(this@toJson) validate(map)
entries.forEach { (key, description) -> map.entries.entries
.sortedWith(
val entry = buildJsonObject { Comparator.comparingInt<Map.Entry<ModbusRegistryKey, String>?> { it.key.sectionNumber }
put( .thenComparingInt { it.key.address }
"type", )
when (key) { .forEach { (key, description) ->
is ModbusRegistryKey.Coil -> "Coil" val typeString = when (key) {
is ModbusRegistryKey.DiscreteInput -> "Discrete" is ModbusRegistryKey.Coil -> "Coil"
is ModbusRegistryKey.HoldingRegister -> "Register" is ModbusRegistryKey.DiscreteInput -> "Discrete"
is ModbusRegistryKey.InputRegister -> "Input" is ModbusRegistryKey.HoldingRegister -> "Register"
is ModbusRegistryKey.InputRegister -> "Input"
}
val rangeString = if (key.count == 1) {
key.address.toString()
} else {
"${key.address} - ${key.address + key.count - 1}"
}
to.appendLine("${typeString}\t$rangeString\t$description")
} }
)
put("address", key.address)
if (key.count > 1) {
put("count", key.count)
}
put("description", description)
} }
add(entry)
} }
} }

View File

@ -63,7 +63,8 @@ public open class OpcUaDeviceBySpec<D : Device>(
} }
} }
override fun onStop() { override fun close() {
client.disconnect() client.disconnect()
super<DeviceBySpec>.close()
} }
} }

View File

@ -73,11 +73,11 @@ public class DeviceNameSpace(
//for now, use DF paths as ids //for now, use DF paths as ids
nodeId = newNodeId("${deviceName.tokens.joinToString("/")}/$propertyName") nodeId = newNodeId("${deviceName.tokens.joinToString("/")}/$propertyName")
when { when {
descriptor.readable && descriptor.mutable -> { descriptor.readable && descriptor.writable -> {
setAccessLevel(AccessLevel.READ_WRITE) setAccessLevel(AccessLevel.READ_WRITE)
setUserAccessLevel(AccessLevel.READ_WRITE) setUserAccessLevel(AccessLevel.READ_WRITE)
} }
descriptor.mutable -> { descriptor.writable -> {
setAccessLevel(AccessLevel.WRITE_ONLY) setAccessLevel(AccessLevel.WRITE_ONLY)
setUserAccessLevel(AccessLevel.WRITE_ONLY) setUserAccessLevel(AccessLevel.WRITE_ONLY)
} }

View File

@ -40,10 +40,9 @@ class OpcUaClientTest {
@Test @Test
@Ignore @Ignore
fun testReadDouble() = runTest { fun testReadDouble() = runTest {
val device = DemoOpcUaDevice.build() DemoOpcUaDevice.build().use{
device.start() println(it.read(DemoOpcUaDevice.randomDouble))
println(device.read(DemoOpcUaDevice.randomDouble)) }
device.stop()
} }
} }

View File

@ -78,7 +78,7 @@ class DemoController : Controller(), ContextAware {
logger.info { "Visualization server stopped" } logger.info { "Visualization server stopped" }
magixServer?.stop(1000, 5000) magixServer?.stop(1000, 5000)
logger.info { "Magix server stopped" } logger.info { "Magix server stopped" }
device?.stop() device?.close()
logger.info { "Device server stopped" } logger.info { "Device server stopped" }
context.close() context.close()
} }

View File

@ -44,7 +44,7 @@ class DemoDevice(context: Context, meta: Meta) : DeviceBySpec<IDemoDevice>(Compa
metaDescriptor { metaDescriptor {
type(ValueType.NUMBER) type(ValueType.NUMBER)
} }
description = "Real to virtual time scale" info = "Real to virtual time scale"
} }
val sinScale by mutableProperty(MetaConverter.double, IDemoDevice::sinScaleState) val sinScale by mutableProperty(MetaConverter.double, IDemoDevice::sinScaleState)

View File

@ -14,6 +14,7 @@ import space.kscience.dataforge.names.Name
import space.kscience.magix.api.MagixEndpoint import space.kscience.magix.api.MagixEndpoint
import space.kscience.magix.api.subscribe import space.kscience.magix.api.subscribe
import space.kscience.magix.rsocket.rSocketWithWebSockets import space.kscience.magix.rsocket.rSocketWithWebSockets
import kotlin.time.ExperimentalTime
class MagixVirtualCar(context: Context, meta: Meta) : VirtualCar(context, meta) { class MagixVirtualCar(context: Context, meta: Meta) : VirtualCar(context, meta) {
@ -30,13 +31,17 @@ class MagixVirtualCar(context: Context, meta: Meta) : VirtualCar(context, meta)
} }
override suspend fun onStart() { @OptIn(ExperimentalTime::class)
override suspend fun open() {
super.open()
val magixEndpoint = MagixEndpoint.rSocketWithWebSockets( val magixEndpoint = MagixEndpoint.rSocketWithWebSockets(
meta["magixServerHost"].string ?: "localhost", meta["magixServerHost"].string ?: "localhost",
) )
magixEndpoint.launchMagixVirtualCarUpdate() launch {
magixEndpoint.launchMagixVirtualCarUpdate()
}
} }
companion object : Factory<MagixVirtualCar> { companion object : Factory<MagixVirtualCar> {

View File

@ -100,7 +100,8 @@ open class VirtualCar(context: Context, meta: Meta) : DeviceBySpec<VirtualCar>(I
} }
@OptIn(ExperimentalTime::class) @OptIn(ExperimentalTime::class)
override suspend fun onStart() { override suspend fun open() {
super<DeviceBySpec>.open()
//initializing the clock //initializing the clock
timeState = Clock.System.now() timeState = Clock.System.now()
//starting regular updates //starting regular updates

View File

@ -71,9 +71,9 @@ class VirtualCarController : Controller(), ContextAware {
logger.info { "Shutting down..." } logger.info { "Shutting down..." }
magixServer?.stop(1000, 5000) magixServer?.stop(1000, 5000)
logger.info { "Magix server stopped" } logger.info { "Magix server stopped" }
magixVirtualCar?.stop() magixVirtualCar?.close()
logger.info { "Magix virtual car server stopped" } logger.info { "Magix virtual car server stopped" }
virtualCar?.stop() virtualCar?.close()
logger.info { "Virtual car server stopped" } logger.info { "Virtual car server stopped" }
context.close() context.close()
} }

View File

@ -138,7 +138,7 @@ class PiMotionMasterDevice(
override fun build(context: Context, meta: Meta): PiMotionMasterDevice = PiMotionMasterDevice(context) override fun build(context: Context, meta: Meta): PiMotionMasterDevice = PiMotionMasterDevice(context)
val connected by booleanProperty(descriptorBuilder = { val connected by booleanProperty(descriptorBuilder = {
description = "True if the connection address is defined and the device is initialized" info = "True if the connection address is defined and the device is initialized"
}) { }) {
port != null port != null
} }
@ -201,7 +201,7 @@ class PiMotionMasterDevice(
val timeout by mutableProperty(MetaConverter.duration, PiMotionMasterDevice::timeoutValue) { val timeout by mutableProperty(MetaConverter.duration, PiMotionMasterDevice::timeoutValue) {
description = "Timeout" info = "Timeout"
} }
} }
@ -267,7 +267,7 @@ class PiMotionMasterDevice(
) )
val enabled by axisBooleanProperty("EAX") { val enabled by axisBooleanProperty("EAX") {
description = "Motor enable state." info = "Motor enable state."
} }
val halt by unitAction { val halt by unitAction {
@ -275,20 +275,20 @@ class PiMotionMasterDevice(
} }
val targetPosition by axisNumberProperty("MOV") { val targetPosition by axisNumberProperty("MOV") {
description = """ info = """
Sets a new absolute target position for the specified axis. Sets a new absolute target position for the specified axis.
Servo mode must be switched on for the commanded axis prior to using this command (closed-loop operation). Servo mode must be switched on for the commanded axis prior to using this command (closed-loop operation).
""".trimIndent() """.trimIndent()
} }
val onTarget by booleanProperty({ val onTarget by booleanProperty({
description = "Queries the on-target state of the specified axis." info = "Queries the on-target state of the specified axis."
}) { }) {
readAxisBoolean("ONT?") readAxisBoolean("ONT?")
} }
val reference by booleanProperty({ val reference by booleanProperty({
description = "Get Referencing Result" info = "Get Referencing Result"
}) { }) {
readAxisBoolean("FRF?") readAxisBoolean("FRF?")
} }
@ -298,36 +298,36 @@ class PiMotionMasterDevice(
} }
val minPosition by doubleProperty({ val minPosition by doubleProperty({
description = "Minimal position value for the axis" info = "Minimal position value for the axis"
}) { }) {
mm.requestAndParse("TMN?", axisId)[axisId]?.toDoubleOrNull() mm.requestAndParse("TMN?", axisId)[axisId]?.toDoubleOrNull()
?: error("Malformed `TMN?` response. Should include float value for $axisId") ?: error("Malformed `TMN?` response. Should include float value for $axisId")
} }
val maxPosition by doubleProperty({ val maxPosition by doubleProperty({
description = "Maximal position value for the axis" info = "Maximal position value for the axis"
}) { }) {
mm.requestAndParse("TMX?", axisId)[axisId]?.toDoubleOrNull() mm.requestAndParse("TMX?", axisId)[axisId]?.toDoubleOrNull()
?: error("Malformed `TMX?` response. Should include float value for $axisId") ?: error("Malformed `TMX?` response. Should include float value for $axisId")
} }
val position by doubleProperty({ val position by doubleProperty({
description = "The current axis position." info = "The current axis position."
}) { }) {
mm.requestAndParse("POS?", axisId)[axisId]?.toDoubleOrNull() mm.requestAndParse("POS?", axisId)[axisId]?.toDoubleOrNull()
?: error("Malformed `POS?` response. Should include float value for $axisId") ?: error("Malformed `POS?` response. Should include float value for $axisId")
} }
val openLoopTarget by axisNumberProperty("OMA") { val openLoopTarget by axisNumberProperty("OMA") {
description = "Position for open-loop operation." info = "Position for open-loop operation."
} }
val closedLoop by axisBooleanProperty("SVO") { val closedLoop by axisBooleanProperty("SVO") {
description = "Servo closed loop mode" info = "Servo closed loop mode"
} }
val velocity by axisNumberProperty("VEL") { val velocity by axisNumberProperty("VEL") {
description = "Velocity value for closed-loop operation" info = "Velocity value for closed-loop operation"
} }
val move by action(MetaConverter.meta, MetaConverter.unit) { val move by action(MetaConverter.meta, MetaConverter.unit) {

View File

@ -10,4 +10,4 @@ publishing.sonatype=false
org.gradle.configureondemand=true org.gradle.configureondemand=true
org.gradle.jvmargs=-Xmx4096m org.gradle.jvmargs=-Xmx4096m
toolsVersion=0.15.0-kotlin-1.9.20-RC2 toolsVersion=0.14.10-kotlin-1.9.0

View File

@ -50,7 +50,6 @@ include(
// ":controls-mongo", // ":controls-mongo",
":controls-storage", ":controls-storage",
":controls-storage:controls-xodus", ":controls-storage:controls-xodus",
":controls-constructor",
":magix", ":magix",
":magix:magix-api", ":magix:magix-api",
":magix:magix-server", ":magix:magix-server",