Clean up property access syntax
This commit is contained in:
parent
86273101b6
commit
691b2ae67a
46
.space.kts
46
.space.kts
@ -1,3 +1,45 @@
|
|||||||
job("Build and run tests") {
|
import kotlin.io.path.readText
|
||||||
gradle("gradle:8.1.1-jdk11", "build")
|
|
||||||
|
job("Build") {
|
||||||
|
gradlew("spc.registry.jetbrains.space/p/sci/containers/kotlin-ci:1.0.3", "build")
|
||||||
|
}
|
||||||
|
|
||||||
|
job("Publish") {
|
||||||
|
startOn {
|
||||||
|
gitPush { enabled = false }
|
||||||
|
}
|
||||||
|
container("spc.registry.jetbrains.space/p/sci/containers/kotlin-ci:1.0.3") {
|
||||||
|
env["SPACE_USER"] = "{{ project:space_user }}"
|
||||||
|
env["SPACE_TOKEN"] = "{{ project:space_token }}"
|
||||||
|
kotlinScript { api ->
|
||||||
|
|
||||||
|
val spaceUser = System.getenv("SPACE_USER")
|
||||||
|
val spaceToken = System.getenv("SPACE_TOKEN")
|
||||||
|
|
||||||
|
// write the version to the build directory
|
||||||
|
api.gradlew("version")
|
||||||
|
|
||||||
|
//read the version from build file
|
||||||
|
val version = java.nio.file.Path.of("build/project-version.txt").readText()
|
||||||
|
|
||||||
|
val revisionSuffix = if (version.endsWith("SNAPSHOT")) {
|
||||||
|
"-" + api.gitRevision().take(7)
|
||||||
|
} else {
|
||||||
|
""
|
||||||
|
}
|
||||||
|
|
||||||
|
api.space().projects.automation.deployments.start(
|
||||||
|
project = api.projectIdentifier(),
|
||||||
|
targetIdentifier = TargetIdentifier.Key("maps-kt"),
|
||||||
|
version = version+revisionSuffix,
|
||||||
|
// automatically update deployment status based on the status of a job
|
||||||
|
syncWithAutomationJob = true
|
||||||
|
)
|
||||||
|
api.gradlew(
|
||||||
|
"publishAllPublicationsToSpaceRepository",
|
||||||
|
"-Ppublishing.space.user=\"$spaceUser\"",
|
||||||
|
"-Ppublishing.space.token=\"$spaceToken\"",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
@ -35,8 +35,4 @@ ksciencePublish {
|
|||||||
space("https://maven.pkg.jetbrains.space/spc/p/controls/maven")
|
space("https://maven.pkg.jetbrains.space/spc/p/controls/maven")
|
||||||
}
|
}
|
||||||
|
|
||||||
readme.readmeTemplate = file("docs/templates/README-TEMPLATE.md")
|
readme.readmeTemplate = file("docs/templates/README-TEMPLATE.md")
|
||||||
|
|
||||||
apiValidation {
|
|
||||||
validationDisabled = true
|
|
||||||
}
|
|
@ -13,6 +13,7 @@ kscience {
|
|||||||
useSerialization{
|
useSerialization{
|
||||||
json()
|
json()
|
||||||
}
|
}
|
||||||
|
useContextReceivers()
|
||||||
dependencies {
|
dependencies {
|
||||||
api("space.kscience:dataforge-io:$dataforgeVersion")
|
api("space.kscience:dataforge-io:$dataforgeVersion")
|
||||||
api(spclibs.kotlinx.datetime)
|
api(spclibs.kotlinx.datetime)
|
||||||
|
@ -4,7 +4,6 @@ import kotlinx.coroutines.Job
|
|||||||
import kotlinx.coroutines.SupervisorJob
|
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.launch
|
|
||||||
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.*
|
||||||
@ -18,7 +17,7 @@ import kotlin.coroutines.CoroutineContext
|
|||||||
* A base abstractions for [Device], introducing specifications for properties
|
* A base abstractions for [Device], introducing specifications for properties
|
||||||
*/
|
*/
|
||||||
@OptIn(InternalDeviceAPI::class)
|
@OptIn(InternalDeviceAPI::class)
|
||||||
public abstract class DeviceBase<D : DeviceBase<D>>(
|
public abstract class DeviceBase<D : Device>(
|
||||||
override val context: Context = Global,
|
override val context: Context = Global,
|
||||||
override val meta: Meta = Meta.EMPTY,
|
override val meta: Meta = Meta.EMPTY,
|
||||||
) : Device {
|
) : Device {
|
||||||
@ -75,7 +74,7 @@ public abstract class DeviceBase<D : DeviceBase<D>>(
|
|||||||
/**
|
/**
|
||||||
* Update logical state using given [spec] and its convertor
|
* Update logical state using given [spec] and its convertor
|
||||||
*/
|
*/
|
||||||
protected suspend fun <T> updateLogical(spec: DevicePropertySpec<D, T>, value: T) {
|
public suspend fun <T> updateLogical(spec: DevicePropertySpec<D, T>, value: T) {
|
||||||
updateLogical(spec.name, spec.converter.objectToMeta(value))
|
updateLogical(spec.name, spec.converter.objectToMeta(value))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -99,7 +98,7 @@ public abstract class DeviceBase<D : DeviceBase<D>>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun writeProperty(propertyName: String, value: Meta): Unit {
|
override suspend fun writeProperty(propertyName: String, value: Meta): Unit {
|
||||||
//If there is a physical property with given name, invalidate logical property and write physical one
|
//If there is a physical property with a given name, invalidate logical property and write physical one
|
||||||
(properties[propertyName] as? WritableDevicePropertySpec<D, out Any?>)?.let {
|
(properties[propertyName] as? WritableDevicePropertySpec<D, out Any?>)?.let {
|
||||||
invalidate(propertyName)
|
invalidate(propertyName)
|
||||||
it.writeMeta(self, value)
|
it.writeMeta(self, value)
|
||||||
@ -111,49 +110,13 @@ public abstract class DeviceBase<D : DeviceBase<D>>(
|
|||||||
override suspend fun execute(action: String, argument: Meta?): Meta? =
|
override suspend fun execute(action: String, argument: Meta?): Meta? =
|
||||||
actions[action]?.executeWithMeta(self, argument)
|
actions[action]?.executeWithMeta(self, argument)
|
||||||
|
|
||||||
/**
|
|
||||||
* Read typed value and update/push event if needed.
|
|
||||||
* Return null if property read is not successful or property is undefined.
|
|
||||||
*/
|
|
||||||
public suspend fun <T> DevicePropertySpec<D, T>.readOrNull(): T? {
|
|
||||||
val res = read(self) ?: return null
|
|
||||||
updateLogical(name, converter.objectToMeta(res))
|
|
||||||
return res
|
|
||||||
}
|
|
||||||
|
|
||||||
public suspend fun <T> DevicePropertySpec<D, T>.read(): T =
|
|
||||||
readOrNull() ?: error("Failed to read property $name state")
|
|
||||||
|
|
||||||
public fun <T> DevicePropertySpec<D, T>.get(): T? = getProperty(name)?.let(converter::metaToObject)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Write typed property state and invalidate logical state
|
|
||||||
*/
|
|
||||||
public suspend fun <T> WritableDevicePropertySpec<D, T>.write(value: T) {
|
|
||||||
invalidate(name)
|
|
||||||
write(self, value)
|
|
||||||
//perform asynchronous read and update after write
|
|
||||||
launch {
|
|
||||||
read()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reset logical state of a property
|
|
||||||
*/
|
|
||||||
public suspend fun DevicePropertySpec<D, *>.invalidate() {
|
|
||||||
invalidate(name)
|
|
||||||
}
|
|
||||||
|
|
||||||
public suspend operator fun <I, O> DeviceActionSpec<D, I, O>.invoke(input: I? = null): O? = execute(self, input)
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A device generated from specification
|
* A device generated from specification
|
||||||
* @param D recursive self-type for properties and actions
|
* @param D recursive self-type for properties and actions
|
||||||
*/
|
*/
|
||||||
public open class DeviceBySpec<D : DeviceBySpec<D>>(
|
public open class DeviceBySpec<D : Device>(
|
||||||
public val spec: DeviceSpec<in D>,
|
public val spec: DeviceSpec<in D>,
|
||||||
context: Context = Global,
|
context: Context = Global,
|
||||||
meta: Meta = Meta.EMPTY,
|
meta: Meta = Meta.EMPTY,
|
||||||
|
@ -91,30 +91,56 @@ public suspend fun <D : Device, I, O> DeviceActionSpec<D, I, O>.executeWithMeta(
|
|||||||
return res?.let { outputConverter.objectToMeta(res) }
|
return res?.let { outputConverter.objectToMeta(res) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
public suspend fun <D : DeviceBase<D>, T : Any> D.read(
|
* Read typed value and update/push event if needed.
|
||||||
propertySpec: DevicePropertySpec<D, T>,
|
* Return null if property read is not successful or property is undefined.
|
||||||
): T = propertySpec.read()
|
*/
|
||||||
|
@OptIn(InternalDeviceAPI::class)
|
||||||
public suspend fun <D : Device, T : Any> D.read(
|
public suspend fun <T, D : Device> D.readOrNull(propertySpec: DevicePropertySpec<D, T>): T? {
|
||||||
propertySpec: DevicePropertySpec<D, T>,
|
val res = propertySpec.read(this) ?: return null
|
||||||
): T = propertySpec.converter.metaToObject(readProperty(propertySpec.name))
|
@Suppress("UNCHECKED_CAST")
|
||||||
?: error("Property meta converter returned null")
|
(this as? DeviceBase<D>)?.updateLogical(propertySpec, res)
|
||||||
|
return res
|
||||||
public fun <D : Device, T> D.write(
|
|
||||||
propertySpec: WritableDevicePropertySpec<D, T>,
|
|
||||||
value: T,
|
|
||||||
): Job = launch {
|
|
||||||
writeProperty(propertySpec.name, propertySpec.converter.objectToMeta(value))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public fun <D : DeviceBase<D>, T> D.write(
|
public suspend fun <T, D : Device> D.read(propertySpec: DevicePropertySpec<D, T>): T =
|
||||||
propertySpec: WritableDevicePropertySpec<D, T>,
|
readOrNull(propertySpec) ?: error("Failed to read property ${propertySpec.name} state")
|
||||||
value: T,
|
|
||||||
): Job = launch {
|
public operator fun <T, D : Device> D.get(propertySpec: DevicePropertySpec<D, T>): T? =
|
||||||
propertySpec.write(value)
|
getProperty(propertySpec.name)?.let(propertySpec.converter::metaToObject)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write typed property state and invalidate logical state
|
||||||
|
*/
|
||||||
|
@OptIn(InternalDeviceAPI::class)
|
||||||
|
public suspend fun <T, D : Device> D.write(propertySpec: WritableDevicePropertySpec<D, T>, value: T) {
|
||||||
|
invalidate(propertySpec.name)
|
||||||
|
propertySpec.write(this, value)
|
||||||
|
//perform asynchronous read and update after write
|
||||||
|
launch {
|
||||||
|
read(propertySpec)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fire and forget variant of property writing. Actual write is performed asynchronously on a [Device] scope
|
||||||
|
*/
|
||||||
|
public operator fun <T, D : Device> D.set(propertySpec: WritableDevicePropertySpec<D, T>, value: T): Unit {
|
||||||
|
launch {
|
||||||
|
write(propertySpec, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset the logical state of a property
|
||||||
|
*/
|
||||||
|
public suspend fun <D : Device> D.invalidate(propertySpec: DevicePropertySpec<D, *>) {
|
||||||
|
invalidate(propertySpec.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
public suspend fun <I, O, D : Device> D.execute(actionSpec: DeviceActionSpec<D, I, O>, input: I? = null): O? =
|
||||||
|
actionSpec.execute(this, input)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A type safe property change listener
|
* A type safe property change listener
|
||||||
*/
|
*/
|
||||||
|
@ -1,10 +0,0 @@
|
|||||||
package space.kscience.controls.spec
|
|
||||||
|
|
||||||
import kotlinx.coroutines.runBlocking
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Blocking property get call
|
|
||||||
*/
|
|
||||||
public operator fun <D : DeviceBase<D>, T : Any> D.get(
|
|
||||||
propertySpec: DevicePropertySpec<D, T>
|
|
||||||
): T? = runBlocking { read(propertySpec) }
|
|
@ -1,6 +1,7 @@
|
|||||||
package space.kscience.controls.modbus
|
package space.kscience.controls.modbus
|
||||||
|
|
||||||
import com.ghgande.j2mod.modbus.facade.AbstractModbusMaster
|
import com.ghgande.j2mod.modbus.facade.AbstractModbusMaster
|
||||||
|
import space.kscience.controls.api.Device
|
||||||
import space.kscience.controls.api.DeviceHub
|
import space.kscience.controls.api.DeviceHub
|
||||||
import space.kscience.controls.spec.DeviceBySpec
|
import space.kscience.controls.spec.DeviceBySpec
|
||||||
import space.kscience.controls.spec.DeviceSpec
|
import space.kscience.controls.spec.DeviceSpec
|
||||||
@ -11,19 +12,19 @@ import space.kscience.dataforge.names.NameToken
|
|||||||
/**
|
/**
|
||||||
* A variant of [DeviceBySpec] that includes Modbus RTU/TCP/UDP client
|
* A variant of [DeviceBySpec] that includes Modbus RTU/TCP/UDP client
|
||||||
*/
|
*/
|
||||||
public open class ModbusDeviceBySpec(
|
public open class ModbusDeviceBySpec<D: Device>(
|
||||||
context: Context,
|
context: Context,
|
||||||
spec: DeviceSpec<ModbusDeviceBySpec>,
|
spec: DeviceSpec<D>,
|
||||||
override val clientId: Int,
|
override val clientId: Int,
|
||||||
override val master: AbstractModbusMaster,
|
override val master: AbstractModbusMaster,
|
||||||
meta: Meta = Meta.EMPTY,
|
meta: Meta = Meta.EMPTY,
|
||||||
) : ModbusDevice, DeviceBySpec<ModbusDeviceBySpec>(spec, context, meta)
|
) : ModbusDevice, DeviceBySpec<D>(spec, context, meta)
|
||||||
|
|
||||||
|
|
||||||
public class ModbusHub(
|
public class ModbusHub(
|
||||||
public val context: Context,
|
public val context: Context,
|
||||||
public val masterBuilder: () -> AbstractModbusMaster,
|
public val masterBuilder: () -> AbstractModbusMaster,
|
||||||
public val specs: Map<NameToken, Pair<Int, DeviceSpec<ModbusDeviceBySpec>>>,
|
public val specs: Map<NameToken, Pair<Int, DeviceSpec<*>>>,
|
||||||
) : DeviceHub, AutoCloseable {
|
) : DeviceHub, AutoCloseable {
|
||||||
|
|
||||||
public val master: AbstractModbusMaster by lazy(masterBuilder)
|
public val master: AbstractModbusMaster by lazy(masterBuilder)
|
||||||
|
@ -12,7 +12,6 @@ dependencies {
|
|||||||
|
|
||||||
api("org.eclipse.milo:sdk-client:$miloVersion")
|
api("org.eclipse.milo:sdk-client:$miloVersion")
|
||||||
api("org.eclipse.milo:bsd-parser:$miloVersion")
|
api("org.eclipse.milo:bsd-parser:$miloVersion")
|
||||||
|
|
||||||
api("org.eclipse.milo:sdk-server:$miloVersion")
|
api("org.eclipse.milo:sdk-server:$miloVersion")
|
||||||
|
|
||||||
testImplementation(spclibs.kotlinx.coroutines.test)
|
testImplementation(spclibs.kotlinx.coroutines.test)
|
||||||
|
@ -18,7 +18,7 @@ import kotlin.reflect.KProperty
|
|||||||
/**
|
/**
|
||||||
* An OPC-UA device backed by Eclipse Milo client
|
* An OPC-UA device backed by Eclipse Milo client
|
||||||
*/
|
*/
|
||||||
public interface MiloDevice : Device {
|
public interface OpcUaDevice : Device {
|
||||||
/**
|
/**
|
||||||
* The OPC-UA client initialized on first use
|
* The OPC-UA client initialized on first use
|
||||||
*/
|
*/
|
||||||
@ -29,7 +29,7 @@ public interface MiloDevice : Device {
|
|||||||
* Read OPC-UA value with timestamp
|
* Read OPC-UA value with timestamp
|
||||||
* @param T the type of property to read. The value is coerced to it.
|
* @param T the type of property to read. The value is coerced to it.
|
||||||
*/
|
*/
|
||||||
public suspend inline fun <reified T: Any> MiloDevice.readOpcWithTime(
|
public suspend inline fun <reified T: Any> OpcUaDevice.readOpcWithTime(
|
||||||
nodeId: NodeId,
|
nodeId: NodeId,
|
||||||
converter: MetaConverter<T>,
|
converter: MetaConverter<T>,
|
||||||
magAge: Double = 500.0
|
magAge: Double = 500.0
|
||||||
@ -50,7 +50,7 @@ public suspend inline fun <reified T: Any> MiloDevice.readOpcWithTime(
|
|||||||
/**
|
/**
|
||||||
* Read and coerce value from OPC-UA
|
* Read and coerce value from OPC-UA
|
||||||
*/
|
*/
|
||||||
public suspend inline fun <reified T> MiloDevice.readOpc(
|
public suspend inline fun <reified T> OpcUaDevice.readOpc(
|
||||||
nodeId: NodeId,
|
nodeId: NodeId,
|
||||||
converter: MetaConverter<T>,
|
converter: MetaConverter<T>,
|
||||||
magAge: Double = 500.0
|
magAge: Double = 500.0
|
||||||
@ -72,7 +72,7 @@ public suspend inline fun <reified T> MiloDevice.readOpc(
|
|||||||
return converter.metaToObject(meta) ?: error("Meta $meta could not be converted to ${T::class}")
|
return converter.metaToObject(meta) ?: error("Meta $meta could not be converted to ${T::class}")
|
||||||
}
|
}
|
||||||
|
|
||||||
public suspend inline fun <reified T> MiloDevice.writeOpc(
|
public suspend inline fun <reified T> OpcUaDevice.writeOpc(
|
||||||
nodeId: NodeId,
|
nodeId: NodeId,
|
||||||
converter: MetaConverter<T>,
|
converter: MetaConverter<T>,
|
||||||
value: T
|
value: T
|
||||||
@ -85,7 +85,7 @@ public suspend inline fun <reified T> MiloDevice.writeOpc(
|
|||||||
/**
|
/**
|
||||||
* A device-bound OPC-UA property. Does not trigger device properties change.
|
* A device-bound OPC-UA property. Does not trigger device properties change.
|
||||||
*/
|
*/
|
||||||
public inline fun <reified T> MiloDevice.opc(
|
public inline fun <reified T> OpcUaDevice.opc(
|
||||||
nodeId: NodeId,
|
nodeId: NodeId,
|
||||||
converter: MetaConverter<T>,
|
converter: MetaConverter<T>,
|
||||||
magAge: Double = 500.0
|
magAge: Double = 500.0
|
||||||
@ -104,7 +104,7 @@ public inline fun <reified T> MiloDevice.opc(
|
|||||||
/**
|
/**
|
||||||
* Register a mutable OPC-UA based [Double] property in a device spec
|
* Register a mutable OPC-UA based [Double] property in a device spec
|
||||||
*/
|
*/
|
||||||
public fun MiloDevice.opcDouble(
|
public fun OpcUaDevice.opcDouble(
|
||||||
nodeId: NodeId,
|
nodeId: NodeId,
|
||||||
magAge: Double = 1.0
|
magAge: Double = 1.0
|
||||||
): ReadWriteProperty<Any?, Double> = opc<Double>(nodeId, MetaConverter.double, magAge)
|
): ReadWriteProperty<Any?, Double> = opc<Double>(nodeId, MetaConverter.double, magAge)
|
||||||
@ -112,7 +112,7 @@ public fun MiloDevice.opcDouble(
|
|||||||
/**
|
/**
|
||||||
* Register a mutable OPC-UA based [Int] property in a device spec
|
* Register a mutable OPC-UA based [Int] property in a device spec
|
||||||
*/
|
*/
|
||||||
public fun MiloDevice.opcInt(
|
public fun OpcUaDevice.opcInt(
|
||||||
nodeId: NodeId,
|
nodeId: NodeId,
|
||||||
magAge: Double = 1.0
|
magAge: Double = 1.0
|
||||||
): ReadWriteProperty<Any?, Int> = opc(nodeId, MetaConverter.int, magAge)
|
): ReadWriteProperty<Any?, Int> = opc(nodeId, MetaConverter.int, magAge)
|
||||||
@ -120,7 +120,7 @@ public fun MiloDevice.opcInt(
|
|||||||
/**
|
/**
|
||||||
* Register a mutable OPC-UA based [String] property in a device spec
|
* Register a mutable OPC-UA based [String] property in a device spec
|
||||||
*/
|
*/
|
||||||
public fun MiloDevice.opcString(
|
public fun OpcUaDevice.opcString(
|
||||||
nodeId: NodeId,
|
nodeId: NodeId,
|
||||||
magAge: Double = 1.0
|
magAge: Double = 1.0
|
||||||
): ReadWriteProperty<Any?, String> = opc(nodeId, MetaConverter.string, magAge)
|
): ReadWriteProperty<Any?, String> = opc(nodeId, MetaConverter.string, magAge)
|
@ -4,6 +4,7 @@ import org.eclipse.milo.opcua.sdk.client.OpcUaClient
|
|||||||
import org.eclipse.milo.opcua.sdk.client.api.identity.AnonymousProvider
|
import org.eclipse.milo.opcua.sdk.client.api.identity.AnonymousProvider
|
||||||
import org.eclipse.milo.opcua.sdk.client.api.identity.UsernameProvider
|
import org.eclipse.milo.opcua.sdk.client.api.identity.UsernameProvider
|
||||||
import org.eclipse.milo.opcua.stack.core.security.SecurityPolicy
|
import org.eclipse.milo.opcua.stack.core.security.SecurityPolicy
|
||||||
|
import space.kscience.controls.api.Device
|
||||||
import space.kscience.controls.spec.DeviceBySpec
|
import space.kscience.controls.spec.DeviceBySpec
|
||||||
import space.kscience.controls.spec.DeviceSpec
|
import space.kscience.controls.spec.DeviceSpec
|
||||||
import space.kscience.dataforge.context.Context
|
import space.kscience.dataforge.context.Context
|
||||||
@ -40,14 +41,14 @@ public class MiloConfiguration : Scheme() {
|
|||||||
/**
|
/**
|
||||||
* A variant of [DeviceBySpec] that includes OPC-UA client
|
* A variant of [DeviceBySpec] that includes OPC-UA client
|
||||||
*/
|
*/
|
||||||
public open class MiloDeviceBySpec<D : MiloDeviceBySpec<D>>(
|
public open class OpcUaDeviceBySpec<D : Device>(
|
||||||
spec: DeviceSpec<D>,
|
spec: DeviceSpec<D>,
|
||||||
config: MiloConfiguration,
|
config: MiloConfiguration,
|
||||||
context: Context = Global,
|
context: Context = Global,
|
||||||
) : MiloDevice, DeviceBySpec<D>(spec, context, config.meta) {
|
) : OpcUaDevice, DeviceBySpec<D>(spec, context, config.meta) {
|
||||||
|
|
||||||
override val client: OpcUaClient by lazy {
|
override val client: OpcUaClient by lazy {
|
||||||
context.createMiloClient(
|
context.createOpcUaClient(
|
||||||
config.endpointUrl,
|
config.endpointUrl,
|
||||||
securityPolicy = config.securityPolicy,
|
securityPolicy = config.securityPolicy,
|
||||||
identityProvider = config.username?.let {
|
identityProvider = config.username?.let {
|
@ -18,10 +18,10 @@ import java.nio.file.Path
|
|||||||
import java.nio.file.Paths
|
import java.nio.file.Paths
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
public fun <T:Any> T?.toOptional(): Optional<T> = if(this == null) Optional.empty() else Optional.of(this)
|
internal fun <T:Any> T?.toOptional(): Optional<T> = if(this == null) Optional.empty() else Optional.of(this)
|
||||||
|
|
||||||
|
|
||||||
internal fun Context.createMiloClient(
|
internal fun Context.createOpcUaClient(
|
||||||
endpointUrl: String, //"opc.tcp://localhost:12686/milo"
|
endpointUrl: String, //"opc.tcp://localhost:12686/milo"
|
||||||
securityPolicy: SecurityPolicy = SecurityPolicy.Basic256Sha256,
|
securityPolicy: SecurityPolicy = SecurityPolicy.Basic256Sha256,
|
||||||
identityProvider: IdentityProvider = AnonymousProvider(),
|
identityProvider: IdentityProvider = AnonymousProvider(),
|
||||||
|
@ -6,27 +6,31 @@ import org.eclipse.milo.opcua.stack.core.types.builtin.NodeId
|
|||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
import space.kscience.controls.spec.DeviceSpec
|
import space.kscience.controls.spec.DeviceSpec
|
||||||
import space.kscience.controls.spec.doubleProperty
|
import space.kscience.controls.spec.doubleProperty
|
||||||
|
import space.kscience.controls.spec.read
|
||||||
import space.kscience.dataforge.meta.transformations.MetaConverter
|
import space.kscience.dataforge.meta.transformations.MetaConverter
|
||||||
|
|
||||||
class OpcUaClientTest {
|
class OpcUaClientTest {
|
||||||
class DemoMiloDevice(config: MiloConfiguration) : MiloDeviceBySpec<DemoMiloDevice>(DemoMiloDevice, config) {
|
class DemoOpcUaDevice(config: MiloConfiguration) : OpcUaDeviceBySpec<DemoOpcUaDevice>(DemoOpcUaDevice, config) {
|
||||||
|
|
||||||
//val randomDouble by opcDouble(NodeId(2, "Dynamic/RandomDouble"))
|
//val randomDouble by opcDouble(NodeId(2, "Dynamic/RandomDouble"))
|
||||||
|
|
||||||
suspend fun readRandomDouble() = readOpc(NodeId(2, "Dynamic/RandomDouble"), MetaConverter.double)
|
suspend fun readRandomDouble() = readOpc(NodeId(2, "Dynamic/RandomDouble"), MetaConverter.double)
|
||||||
|
|
||||||
|
|
||||||
companion object : DeviceSpec<DemoMiloDevice>() {
|
companion object : DeviceSpec<DemoOpcUaDevice>() {
|
||||||
fun build(): DemoMiloDevice {
|
/**
|
||||||
|
* Build a device. This is not a part of the specification
|
||||||
|
*/
|
||||||
|
fun build(): DemoOpcUaDevice {
|
||||||
val config = MiloConfiguration {
|
val config = MiloConfiguration {
|
||||||
endpointUrl = "opc.tcp://milo.digitalpetri.com:62541/milo"
|
endpointUrl = "opc.tcp://milo.digitalpetri.com:62541/milo"
|
||||||
}
|
}
|
||||||
return DemoMiloDevice(config)
|
return DemoOpcUaDevice(config)
|
||||||
}
|
}
|
||||||
|
|
||||||
inline fun <R> use(block: DemoMiloDevice.() -> R): R = build().use(block)
|
inline fun <R> use(block: (DemoOpcUaDevice) -> R): R = build().use(block)
|
||||||
|
|
||||||
val randomDouble by doubleProperty(read = DemoMiloDevice::readRandomDouble)
|
val randomDouble by doubleProperty(read = DemoOpcUaDevice::readRandomDouble)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -36,7 +40,9 @@ class OpcUaClientTest {
|
|||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
@Test
|
@Test
|
||||||
fun testReadDouble() = runTest {
|
fun testReadDouble() = runTest {
|
||||||
println(DemoMiloDevice.use { DemoMiloDevice.randomDouble.read() })
|
DemoOpcUaDevice.use{
|
||||||
|
println(it.read(DemoOpcUaDevice.randomDouble))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -13,11 +13,11 @@ import kotlinx.serialization.json.jsonObject
|
|||||||
import kotlinx.serialization.json.jsonPrimitive
|
import kotlinx.serialization.json.jsonPrimitive
|
||||||
import space.kscience.controls.api.DeviceMessage
|
import space.kscience.controls.api.DeviceMessage
|
||||||
import space.kscience.controls.storage.DeviceMessageStorage
|
import space.kscience.controls.storage.DeviceMessageStorage
|
||||||
import space.kscience.controls.storage.workDirectory
|
|
||||||
import space.kscience.dataforge.context.Context
|
import space.kscience.dataforge.context.Context
|
||||||
import space.kscience.dataforge.context.Factory
|
import space.kscience.dataforge.context.Factory
|
||||||
import space.kscience.dataforge.context.request
|
import space.kscience.dataforge.context.request
|
||||||
import space.kscience.dataforge.io.IOPlugin
|
import space.kscience.dataforge.io.IOPlugin
|
||||||
|
import space.kscience.dataforge.io.workDirectory
|
||||||
import space.kscience.dataforge.meta.Meta
|
import space.kscience.dataforge.meta.Meta
|
||||||
import space.kscience.dataforge.meta.get
|
import space.kscience.dataforge.meta.get
|
||||||
import space.kscience.dataforge.meta.string
|
import space.kscience.dataforge.meta.string
|
||||||
|
@ -1,29 +0,0 @@
|
|||||||
package space.kscience.controls.storage
|
|
||||||
|
|
||||||
import space.kscience.dataforge.context.ContextBuilder
|
|
||||||
import space.kscience.dataforge.io.IOPlugin
|
|
||||||
import space.kscience.dataforge.meta.get
|
|
||||||
import space.kscience.dataforge.meta.set
|
|
||||||
import space.kscience.dataforge.meta.string
|
|
||||||
import java.nio.file.Path
|
|
||||||
import kotlin.io.path.Path
|
|
||||||
|
|
||||||
|
|
||||||
public val IOPlugin.workDirectory: Path
|
|
||||||
get() {
|
|
||||||
val workDirectoryPath = meta[IOPlugin.WORK_DIRECTORY_KEY].string
|
|
||||||
?: context.properties[IOPlugin.WORK_DIRECTORY_KEY].string
|
|
||||||
?: ".dataforge"
|
|
||||||
|
|
||||||
return Path(workDirectoryPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
public fun ContextBuilder.workDirectory(path: String) {
|
|
||||||
properties {
|
|
||||||
set(IOPlugin.WORK_DIRECTORY_KEY, path)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public fun ContextBuilder.workDirectory(path: Path){
|
|
||||||
workDirectory(path.toAbsolutePath().toString())
|
|
||||||
}
|
|
@ -17,6 +17,7 @@ import space.kscience.controls.manager.install
|
|||||||
import space.kscience.controls.opcua.server.OpcUaServer
|
import space.kscience.controls.opcua.server.OpcUaServer
|
||||||
import space.kscience.controls.opcua.server.endpoint
|
import space.kscience.controls.opcua.server.endpoint
|
||||||
import space.kscience.controls.opcua.server.serveDevices
|
import space.kscience.controls.opcua.server.serveDevices
|
||||||
|
import space.kscience.controls.spec.write
|
||||||
import space.kscience.dataforge.context.*
|
import space.kscience.dataforge.context.*
|
||||||
import space.kscience.magix.api.MagixEndpoint
|
import space.kscience.magix.api.MagixEndpoint
|
||||||
import space.kscience.magix.rsocket.rSocketWithTcp
|
import space.kscience.magix.rsocket.rSocketWithTcp
|
||||||
@ -125,9 +126,9 @@ class DemoControllerView : View(title = " Demo controller remote") {
|
|||||||
action {
|
action {
|
||||||
controller.device?.run {
|
controller.device?.run {
|
||||||
launch {
|
launch {
|
||||||
timeScale.write(timeScaleSlider.value)
|
write(timeScale, timeScaleSlider.value)
|
||||||
sinScale.write(xScaleSlider.value)
|
write(sinScale, xScaleSlider.value)
|
||||||
cosScale.write(yScaleSlider.value)
|
write(cosScale, yScaleSlider.value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -62,21 +62,21 @@ class DemoDevice(context: Context, meta: Meta) : DeviceBySpec<DemoDevice>(DemoDe
|
|||||||
|
|
||||||
override suspend fun DemoDevice.onOpen() {
|
override suspend fun DemoDevice.onOpen() {
|
||||||
launch {
|
launch {
|
||||||
sinScale.read()
|
read(sinScale)
|
||||||
cosScale.read()
|
read(cosScale)
|
||||||
timeScale.read()
|
read(timeScale)
|
||||||
}
|
}
|
||||||
doRecurring(50.milliseconds) {
|
doRecurring(50.milliseconds) {
|
||||||
sin.read()
|
read(sin)
|
||||||
cos.read()
|
read(cos)
|
||||||
coordinates.read()
|
read(coordinates)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val resetScale by action(MetaConverter.meta, MetaConverter.meta) {
|
val resetScale by action(MetaConverter.meta, MetaConverter.meta) {
|
||||||
timeScale.write(5000.0)
|
write(timeScale, 5000.0)
|
||||||
sinScale.write(1.0)
|
write(sinScale, 1.0)
|
||||||
cosScale.write(1.0)
|
write(cosScale, 1.0)
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,6 +3,7 @@ package space.kscience.controls.demo.car
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import space.kscience.controls.api.PropertyChangedMessage
|
import space.kscience.controls.api.PropertyChangedMessage
|
||||||
import space.kscience.controls.client.controlsMagixFormat
|
import space.kscience.controls.client.controlsMagixFormat
|
||||||
|
import space.kscience.controls.spec.write
|
||||||
import space.kscience.dataforge.context.Context
|
import space.kscience.dataforge.context.Context
|
||||||
import space.kscience.dataforge.context.Factory
|
import space.kscience.dataforge.context.Factory
|
||||||
import space.kscience.dataforge.meta.Meta
|
import space.kscience.dataforge.meta.Meta
|
||||||
@ -21,7 +22,7 @@ class MagixVirtualCar(context: Context, meta: Meta) : VirtualCar(context, meta)
|
|||||||
(payload as? PropertyChangedMessage)?.let { message ->
|
(payload as? PropertyChangedMessage)?.let { message ->
|
||||||
if (message.sourceDevice == Name.parse("virtual-car")) {
|
if (message.sourceDevice == Name.parse("virtual-car")) {
|
||||||
when (message.property) {
|
when (message.property) {
|
||||||
"acceleration" -> IVirtualCar.acceleration.write(Vector2D.metaToObject(message.value))
|
"acceleration" -> write(IVirtualCar.acceleration, Vector2D.metaToObject(message.value))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,7 @@ import kotlinx.datetime.Clock
|
|||||||
import kotlinx.datetime.Instant
|
import kotlinx.datetime.Instant
|
||||||
import space.kscience.controls.spec.DeviceBySpec
|
import space.kscience.controls.spec.DeviceBySpec
|
||||||
import space.kscience.controls.spec.doRecurring
|
import space.kscience.controls.spec.doRecurring
|
||||||
|
import space.kscience.controls.spec.read
|
||||||
import space.kscience.dataforge.context.Context
|
import space.kscience.dataforge.context.Context
|
||||||
import space.kscience.dataforge.context.Factory
|
import space.kscience.dataforge.context.Factory
|
||||||
import space.kscience.dataforge.meta.Meta
|
import space.kscience.dataforge.meta.Meta
|
||||||
@ -78,9 +79,9 @@ open class VirtualCar(context: Context, meta: Meta) : DeviceBySpec<VirtualCar>(I
|
|||||||
//TODO apply friction. One can introduce rotation of the cabin and different friction coefficients along the axis
|
//TODO apply friction. One can introduce rotation of the cabin and different friction coefficients along the axis
|
||||||
launch {
|
launch {
|
||||||
//update logical states
|
//update logical states
|
||||||
IVirtualCar.location.read()
|
read(IVirtualCar.location)
|
||||||
IVirtualCar.speed.read()
|
read(IVirtualCar.speed)
|
||||||
IVirtualCar.acceleration.read()
|
read(IVirtualCar.acceleration)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -12,6 +12,7 @@ import space.kscience.controls.client.connectToMagix
|
|||||||
import space.kscience.controls.demo.car.IVirtualCar.Companion.acceleration
|
import space.kscience.controls.demo.car.IVirtualCar.Companion.acceleration
|
||||||
import space.kscience.controls.manager.DeviceManager
|
import space.kscience.controls.manager.DeviceManager
|
||||||
import space.kscience.controls.manager.install
|
import space.kscience.controls.manager.install
|
||||||
|
import space.kscience.controls.spec.write
|
||||||
import space.kscience.controls.storage.storeMessages
|
import space.kscience.controls.storage.storeMessages
|
||||||
import space.kscience.controls.xodus.XodusDeviceMessageStorage
|
import space.kscience.controls.xodus.XodusDeviceMessageStorage
|
||||||
import space.kscience.dataforge.context.*
|
import space.kscience.dataforge.context.*
|
||||||
@ -113,7 +114,8 @@ class VirtualCarControllerView : View(title = " Virtual car controller remote")
|
|||||||
action {
|
action {
|
||||||
controller.virtualCar?.run {
|
controller.virtualCar?.run {
|
||||||
launch {
|
launch {
|
||||||
acceleration.write(
|
write(
|
||||||
|
acceleration,
|
||||||
Vector2D(
|
Vector2D(
|
||||||
accelerationXProperty.get(),
|
accelerationXProperty.get(),
|
||||||
accelerationYProperty.get()
|
accelerationYProperty.get()
|
||||||
|
@ -45,7 +45,7 @@ class MksPdr900Device(context: Context, meta: Meta) : DeviceBySpec<MksPdr900Devi
|
|||||||
|
|
||||||
|
|
||||||
public suspend fun writePowerOn(powerOnValue: Boolean) {
|
public suspend fun writePowerOn(powerOnValue: Boolean) {
|
||||||
error.invalidate()
|
invalidate(error)
|
||||||
if (powerOnValue) {
|
if (powerOnValue) {
|
||||||
val ans = talk("FP!ON")
|
val ans = talk("FP!ON")
|
||||||
if (ans == "ON") {
|
if (ans == "ON") {
|
||||||
@ -65,7 +65,7 @@ class MksPdr900Device(context: Context, meta: Meta) : DeviceBySpec<MksPdr900Devi
|
|||||||
|
|
||||||
public suspend fun readChannelData(channel: Int): Double? {
|
public suspend fun readChannelData(channel: Int): Double? {
|
||||||
val answer: String? = talk("PR$channel?")
|
val answer: String? = talk("PR$channel?")
|
||||||
error.invalidate()
|
invalidate(error)
|
||||||
return if (answer.isNullOrEmpty()) {
|
return if (answer.isNullOrEmpty()) {
|
||||||
// updateState(PortSensor.CONNECTED_STATE, false)
|
// updateState(PortSensor.CONNECTED_STATE, false)
|
||||||
updateLogical(error, "No connection")
|
updateLogical(error, "No connection")
|
||||||
@ -94,7 +94,7 @@ class MksPdr900Device(context: Context, meta: Meta) : DeviceBySpec<MksPdr900Devi
|
|||||||
val channel by logicalProperty(MetaConverter.int)
|
val channel by logicalProperty(MetaConverter.int)
|
||||||
|
|
||||||
val value by doubleProperty(read = {
|
val value by doubleProperty(read = {
|
||||||
readChannelData(channel.get() ?: DEFAULT_CHANNEL)
|
readChannelData(get(channel) ?: DEFAULT_CHANNEL)
|
||||||
})
|
})
|
||||||
|
|
||||||
val error by logicalProperty(MetaConverter.string)
|
val error by logicalProperty(MetaConverter.string)
|
||||||
|
@ -16,6 +16,7 @@ import ru.mipt.npm.devices.pimotionmaster.PiMotionMasterDevice.Axis.Companion.mi
|
|||||||
import ru.mipt.npm.devices.pimotionmaster.PiMotionMasterDevice.Axis.Companion.position
|
import ru.mipt.npm.devices.pimotionmaster.PiMotionMasterDevice.Axis.Companion.position
|
||||||
import space.kscience.controls.manager.DeviceManager
|
import space.kscience.controls.manager.DeviceManager
|
||||||
import space.kscience.controls.manager.installing
|
import space.kscience.controls.manager.installing
|
||||||
|
import space.kscience.controls.spec.read
|
||||||
import space.kscience.dataforge.context.Context
|
import space.kscience.dataforge.context.Context
|
||||||
import space.kscience.dataforge.context.request
|
import space.kscience.dataforge.context.request
|
||||||
import tornadofx.*
|
import tornadofx.*
|
||||||
@ -44,10 +45,10 @@ fun VBox.piMotionMasterAxis(
|
|||||||
label(axisName)
|
label(axisName)
|
||||||
coroutineScope.launch {
|
coroutineScope.launch {
|
||||||
with(axis) {
|
with(axis) {
|
||||||
val min: Double = minPosition.read()
|
val min: Double = read(minPosition)
|
||||||
val max: Double = maxPosition.read()
|
val max: Double = read(maxPosition)
|
||||||
val positionProperty = fxProperty(position)
|
val positionProperty = fxProperty(position)
|
||||||
val startPosition = position.read()
|
val startPosition = read(position)
|
||||||
runLater {
|
runLater {
|
||||||
vbox {
|
vbox {
|
||||||
hgrow = Priority.ALWAYS
|
hgrow = Priority.ALWAYS
|
||||||
|
@ -39,7 +39,7 @@ class PiMotionMasterDevice(
|
|||||||
|
|
||||||
fun disconnect() {
|
fun disconnect() {
|
||||||
runBlocking {
|
runBlocking {
|
||||||
disconnect.invoke()
|
execute(disconnect)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -60,7 +60,7 @@ class PiMotionMasterDevice(
|
|||||||
|
|
||||||
fun connect(host: String, port: Int) {
|
fun connect(host: String, port: Int) {
|
||||||
runBlocking {
|
runBlocking {
|
||||||
connect(Meta {
|
execute(connect, Meta {
|
||||||
"host" put host
|
"host" put host
|
||||||
"port" put port
|
"port" put port
|
||||||
})
|
})
|
||||||
@ -176,7 +176,7 @@ class PiMotionMasterDevice(
|
|||||||
// connector.open()
|
// connector.open()
|
||||||
//Initialize axes
|
//Initialize axes
|
||||||
if (portSpec != null) {
|
if (portSpec != null) {
|
||||||
val idn = identity.read()
|
val idn = read(identity)
|
||||||
failIfError { "Can't connect to $portSpec. Error code: $it" }
|
failIfError { "Can't connect to $portSpec. Error code: $it" }
|
||||||
logger.info { "Connected to $idn on $portSpec" }
|
logger.info { "Connected to $idn on $portSpec" }
|
||||||
val ids = request("SAI?").map { it.trim() }
|
val ids = request("SAI?").map { it.trim() }
|
||||||
@ -185,7 +185,7 @@ class PiMotionMasterDevice(
|
|||||||
axes = ids.associateWith { Axis(this, it) }
|
axes = ids.associateWith { Axis(this, it) }
|
||||||
}
|
}
|
||||||
Meta(ids.map { it.asValue() }.asValue())
|
Meta(ids.map { it.asValue() }.asValue())
|
||||||
initialize()
|
execute(initialize)
|
||||||
failIfError()
|
failIfError()
|
||||||
}
|
}
|
||||||
null
|
null
|
||||||
@ -195,7 +195,7 @@ class PiMotionMasterDevice(
|
|||||||
info = "Disconnect the program from the device if it is connected"
|
info = "Disconnect the program from the device if it is connected"
|
||||||
}) {
|
}) {
|
||||||
port?.let{
|
port?.let{
|
||||||
stop()
|
execute(stop)
|
||||||
it.close()
|
it.close()
|
||||||
}
|
}
|
||||||
port = null
|
port = null
|
||||||
@ -237,7 +237,7 @@ class PiMotionMasterDevice(
|
|||||||
}
|
}
|
||||||
|
|
||||||
suspend fun move(target: Double) {
|
suspend fun move(target: Double) {
|
||||||
move(target.asMeta())
|
execute(move, target.asMeta())
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object : DeviceSpec<Axis>() {
|
companion object : DeviceSpec<Axis>() {
|
||||||
@ -336,15 +336,15 @@ class PiMotionMasterDevice(
|
|||||||
|
|
||||||
val move by metaAction {
|
val move by metaAction {
|
||||||
val target = it.double ?: it?.get("target").double ?: error("Unacceptable target value $it")
|
val target = it.double ?: it?.get("target").double ?: error("Unacceptable target value $it")
|
||||||
closedLoop.write(true)
|
write(closedLoop, true)
|
||||||
//optionally set velocity
|
//optionally set velocity
|
||||||
it?.get("velocity").double?.let { v ->
|
it?.get("velocity").double?.let { v ->
|
||||||
velocity.write(v)
|
write(velocity, v)
|
||||||
}
|
}
|
||||||
targetPosition.write(target)
|
write(targetPosition, target)
|
||||||
//read `onTarget` and `position` properties in a cycle until movement is complete
|
//read `onTarget` and `position` properties in a cycle until movement is complete
|
||||||
while (!onTarget.read()) {
|
while (!read(onTarget)) {
|
||||||
position.read()
|
read(position)
|
||||||
delay(200)
|
delay(200)
|
||||||
}
|
}
|
||||||
null
|
null
|
||||||
|
@ -59,7 +59,7 @@ fun <D : Device, T : Any> D.fxProperty(spec: WritableDevicePropertySpec<D, T>):
|
|||||||
|
|
||||||
onChange { newValue ->
|
onChange { newValue ->
|
||||||
if (newValue != null) {
|
if (newValue != null) {
|
||||||
write(spec, newValue)
|
set(spec, newValue)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user