Update and finalize message design
This commit is contained in:
parent
8f9bae6462
commit
dbf0466c64
@ -0,0 +1,34 @@
|
|||||||
|
package hep.dataforge.control.api
|
||||||
|
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.collect
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.io.Closeable
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A generic bi-directional sender/receiver object
|
||||||
|
*/
|
||||||
|
public interface Socket<T> : Closeable {
|
||||||
|
/**
|
||||||
|
* Send an object to the socket
|
||||||
|
*/
|
||||||
|
public suspend fun send(data: T)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Flow of objects received from socket
|
||||||
|
*/
|
||||||
|
public fun receiving(): Flow<T>
|
||||||
|
public fun isOpen(): Boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Connect an input to this socket using designated [scope] for it and return a handler [Job].
|
||||||
|
* Multiple inputs could be connected to the same [Socket].
|
||||||
|
*/
|
||||||
|
public fun <T> Socket<T>.connectInput(scope: CoroutineScope, flow: Flow<T>): Job = scope.launch {
|
||||||
|
flow.collect { send(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -41,12 +41,10 @@ public class DeviceController(
|
|||||||
if (value == null) return
|
if (value == null) return
|
||||||
scope.launch {
|
scope.launch {
|
||||||
val change = DeviceMessage.ok {
|
val change = DeviceMessage.ok {
|
||||||
this.source = deviceTarget
|
this.sourceName = deviceTarget
|
||||||
type = PROPERTY_CHANGED_ACTION
|
this.action = PROPERTY_CHANGED_ACTION
|
||||||
data {
|
this.key = propertyName
|
||||||
name = propertyName
|
this.value = value
|
||||||
this.value = value
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
val envelope = SimpleEnvelope(change.toMeta(), Binary.EMPTY)
|
val envelope = SimpleEnvelope(change.toMeta(), Binary.EMPTY)
|
||||||
|
|
||||||
@ -98,62 +96,52 @@ public class DeviceController(
|
|||||||
request: DeviceMessage,
|
request: DeviceMessage,
|
||||||
): DeviceMessage {
|
): DeviceMessage {
|
||||||
return try {
|
return try {
|
||||||
val result: List<MessageData> = when (val action = request.type) {
|
|
||||||
GET_PROPERTY_ACTION -> {
|
|
||||||
request.data.map { property ->
|
|
||||||
MessageData {
|
|
||||||
name = property.name
|
|
||||||
value = device.getProperty(name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
SET_PROPERTY_ACTION -> {
|
|
||||||
request.data.map { property ->
|
|
||||||
val propertyName: String = property.name
|
|
||||||
val propertyValue = property.value
|
|
||||||
if (propertyValue == null) {
|
|
||||||
device.invalidateProperty(propertyName)
|
|
||||||
} else {
|
|
||||||
device.setProperty(propertyName, propertyValue)
|
|
||||||
}
|
|
||||||
MessageData {
|
|
||||||
name = propertyName
|
|
||||||
value = device.getProperty(propertyName)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
EXECUTE_ACTION -> {
|
|
||||||
request.data.map { payload ->
|
|
||||||
MessageData {
|
|
||||||
name = payload.name
|
|
||||||
value = device.execute(payload.name, payload.value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
PROPERTY_LIST_ACTION -> {
|
|
||||||
device.propertyDescriptors.map { descriptor ->
|
|
||||||
MessageData {
|
|
||||||
name = descriptor.name
|
|
||||||
value = MetaItem.NodeItem(descriptor.config)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ACTION_LIST_ACTION -> {
|
|
||||||
device.actionDescriptors.map { descriptor ->
|
|
||||||
MessageData {
|
|
||||||
name = descriptor.name
|
|
||||||
value = MetaItem.NodeItem(descriptor.config)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
error("Unrecognized action $action")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
DeviceMessage.ok {
|
DeviceMessage.ok {
|
||||||
target = request.source
|
targetName = request.sourceName
|
||||||
source = deviceTarget
|
sourceName = deviceTarget
|
||||||
data = result
|
action ="response.${request.action}"
|
||||||
|
val requestKey = request.key
|
||||||
|
val requestValue = request.value
|
||||||
|
|
||||||
|
when (val action = request.action) {
|
||||||
|
GET_PROPERTY_ACTION -> {
|
||||||
|
key = requestKey
|
||||||
|
value = device.getProperty(requestKey ?: error("Key field is not defined in request"))
|
||||||
|
}
|
||||||
|
SET_PROPERTY_ACTION -> {
|
||||||
|
require(requestKey != null) { "Key field is not defined in request" }
|
||||||
|
if (requestValue == null) {
|
||||||
|
device.invalidateProperty(requestKey)
|
||||||
|
} else {
|
||||||
|
device.setProperty(requestKey, requestValue)
|
||||||
|
}
|
||||||
|
key = requestKey
|
||||||
|
value = device.getProperty(requestKey)
|
||||||
|
}
|
||||||
|
EXECUTE_ACTION -> {
|
||||||
|
require(requestKey != null) { "Key field is not defined in request" }
|
||||||
|
key = requestKey
|
||||||
|
value = device.execute(requestKey, requestValue)
|
||||||
|
|
||||||
|
}
|
||||||
|
PROPERTY_LIST_ACTION -> {
|
||||||
|
value = Meta {
|
||||||
|
device.propertyDescriptors.map { descriptor ->
|
||||||
|
descriptor.name put descriptor.config
|
||||||
|
}
|
||||||
|
}.asMetaItem()
|
||||||
|
}
|
||||||
|
ACTION_LIST_ACTION -> {
|
||||||
|
value = Meta {
|
||||||
|
device.actionDescriptors.map { descriptor ->
|
||||||
|
descriptor.name put descriptor.config
|
||||||
|
}
|
||||||
|
}.asMetaItem()
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
error("Unrecognized action $action")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (ex: Exception) {
|
} catch (ex: Exception) {
|
||||||
DeviceMessage.fail(request, cause = ex)
|
DeviceMessage.fail(request, cause = ex)
|
||||||
@ -165,7 +153,7 @@ public class DeviceController(
|
|||||||
|
|
||||||
public suspend fun DeviceHub.respondMessage(request: DeviceMessage): DeviceMessage {
|
public suspend fun DeviceHub.respondMessage(request: DeviceMessage): DeviceMessage {
|
||||||
return try {
|
return try {
|
||||||
val targetName = request.target?.toName() ?: Name.EMPTY
|
val targetName = request.targetName?.toName() ?: Name.EMPTY
|
||||||
val device = this[targetName] ?: error("The device with name $targetName not found in $this")
|
val device = this[targetName] ?: error("The device with name $targetName not found in $this")
|
||||||
DeviceController.respondMessage(device, targetName.toString(), request)
|
DeviceController.respondMessage(device, targetName.toString(), request)
|
||||||
} catch (ex: Exception) {
|
} catch (ex: Exception) {
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
package hep.dataforge.control.controllers
|
package hep.dataforge.control.controllers
|
||||||
|
|
||||||
import hep.dataforge.control.controllers.DeviceController.Companion.GET_PROPERTY_ACTION
|
|
||||||
import hep.dataforge.io.SimpleEnvelope
|
import hep.dataforge.io.SimpleEnvelope
|
||||||
import hep.dataforge.meta.*
|
import hep.dataforge.meta.*
|
||||||
import hep.dataforge.names.Name
|
import hep.dataforge.names.Name
|
||||||
@ -11,28 +10,20 @@ import kotlinx.serialization.encoding.Decoder
|
|||||||
import kotlinx.serialization.encoding.Encoder
|
import kotlinx.serialization.encoding.Encoder
|
||||||
|
|
||||||
public class DeviceMessage : Scheme() {
|
public class DeviceMessage : Scheme() {
|
||||||
public var source: String? by string(key = SOURCE_KEY)
|
public var action: String by string { error("Action not defined") }
|
||||||
public var target: String? by string(key = TARGET_KEY)
|
public var status: String by string(default = RESPONSE_OK_STATUS)
|
||||||
public var type: String by string(default = GET_PROPERTY_ACTION, key = MESSAGE_TYPE_KEY)
|
public var sourceName: String? by string()
|
||||||
|
public var targetName: String? by string()
|
||||||
public var comment: String? by string()
|
public var comment: String? by string()
|
||||||
public var status: String by string(RESPONSE_OK_STATUS)
|
public var key: String? by string()
|
||||||
public var data: List<MessageData>
|
public var value: MetaItem<*>? by item()
|
||||||
get() = config.getIndexed(MESSAGE_DATA_KEY).values.map { MessageData.wrap(it.node!!) }
|
|
||||||
set(value) {
|
|
||||||
config[MESSAGE_DATA_KEY] = value.map { it.config }
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Append a payload to this message according to the given scheme
|
|
||||||
*/
|
|
||||||
public fun <T : Configurable> append(spec: Specification<T>, block: T.() -> Unit): T =
|
|
||||||
spec.invoke(block).also { config.append(MESSAGE_DATA_KEY, it) }
|
|
||||||
|
|
||||||
public companion object : SchemeSpec<DeviceMessage>(::DeviceMessage), KSerializer<DeviceMessage> {
|
public companion object : SchemeSpec<DeviceMessage>(::DeviceMessage), KSerializer<DeviceMessage> {
|
||||||
public val SOURCE_KEY: Name = "source".asName()
|
public val SOURCE_KEY: Name = DeviceMessage::sourceName.name.asName()
|
||||||
public val TARGET_KEY: Name = "target".asName()
|
public val TARGET_KEY: Name = DeviceMessage::targetName.name.asName()
|
||||||
public val MESSAGE_TYPE_KEY: Name = "type".asName()
|
public val MESSAGE_ACTION_KEY: Name = DeviceMessage::action.name.asName()
|
||||||
public val MESSAGE_DATA_KEY: Name = "data".asName()
|
public val MESSAGE_KEY_KEY: Name = DeviceMessage::key.name.asName()
|
||||||
|
public val MESSAGE_VALUE_KEY: Name = DeviceMessage::value.name.asName()
|
||||||
|
|
||||||
public const val RESPONSE_OK_STATUS: String = "response.OK"
|
public const val RESPONSE_OK_STATUS: String = "response.OK"
|
||||||
public const val RESPONSE_FAIL_STATUS: String = "response.FAIL"
|
public const val RESPONSE_FAIL_STATUS: String = "response.FAIL"
|
||||||
@ -40,19 +31,19 @@ public class DeviceMessage : Scheme() {
|
|||||||
|
|
||||||
public inline fun ok(
|
public inline fun ok(
|
||||||
request: DeviceMessage? = null,
|
request: DeviceMessage? = null,
|
||||||
block: DeviceMessage.() -> Unit = {}
|
block: DeviceMessage.() -> Unit = {},
|
||||||
): DeviceMessage = DeviceMessage {
|
): DeviceMessage = DeviceMessage {
|
||||||
target = request?.source
|
targetName = request?.sourceName
|
||||||
}.apply(block)
|
}.apply(block)
|
||||||
|
|
||||||
public inline fun fail(
|
public inline fun fail(
|
||||||
request: DeviceMessage? = null,
|
request: DeviceMessage? = null,
|
||||||
cause: Throwable? = null,
|
cause: Throwable? = null,
|
||||||
block: DeviceMessage.() -> Unit = {}
|
block: DeviceMessage.() -> Unit = {},
|
||||||
): DeviceMessage = DeviceMessage {
|
): DeviceMessage = DeviceMessage {
|
||||||
target = request?.source
|
targetName = request?.sourceName
|
||||||
status = RESPONSE_FAIL_STATUS
|
status = RESPONSE_FAIL_STATUS
|
||||||
if(cause!=null){
|
if (cause != null) {
|
||||||
configure {
|
configure {
|
||||||
set("error.type", cause::class.simpleName)
|
set("error.type", cause::class.simpleName)
|
||||||
set("error.message", cause.message)
|
set("error.message", cause.message)
|
||||||
@ -76,16 +67,4 @@ public class DeviceMessage : Scheme() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class MessageData : Scheme() {
|
|
||||||
public var name: String by string { error("Property name could not be empty") }
|
|
||||||
public var value: MetaItem<*>? by item(key = DATA_VALUE_KEY)
|
|
||||||
|
|
||||||
public companion object : SchemeSpec<MessageData>(::MessageData) {
|
|
||||||
public val DATA_VALUE_KEY: Name = "value".asName()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@DFBuilder
|
|
||||||
public fun DeviceMessage.data(block: MessageData.() -> Unit): MessageData = append(MessageData, block)
|
|
||||||
|
|
||||||
public fun DeviceMessage.wrap(): SimpleEnvelope = SimpleEnvelope(this.config, null)
|
public fun DeviceMessage.wrap(): SimpleEnvelope = SimpleEnvelope(this.config, null)
|
||||||
|
@ -21,7 +21,7 @@ import kotlinx.coroutines.launch
|
|||||||
@OptIn(DFExperimental::class)
|
@OptIn(DFExperimental::class)
|
||||||
public class HubController(
|
public class HubController(
|
||||||
public val hub: DeviceHub,
|
public val hub: DeviceHub,
|
||||||
public val scope: CoroutineScope
|
public val scope: CoroutineScope,
|
||||||
) : Consumer, Responder {
|
) : Consumer, Responder {
|
||||||
|
|
||||||
private val messageOutbox = Channel<DeviceMessage>(Channel.CONFLATED)
|
private val messageOutbox = Channel<DeviceMessage>(Channel.CONFLATED)
|
||||||
@ -45,12 +45,10 @@ public class HubController(
|
|||||||
if (value == null) return
|
if (value == null) return
|
||||||
scope.launch {
|
scope.launch {
|
||||||
val change = DeviceMessage.ok {
|
val change = DeviceMessage.ok {
|
||||||
source = name.toString()
|
sourceName = name.toString()
|
||||||
type = DeviceMessage.PROPERTY_CHANGED_ACTION
|
action = DeviceMessage.PROPERTY_CHANGED_ACTION
|
||||||
data {
|
this.key = propertyName
|
||||||
this.name = propertyName
|
this.value = value
|
||||||
this.value = value
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
messageOutbox.send(change)
|
messageOutbox.send(change)
|
||||||
@ -62,7 +60,7 @@ public class HubController(
|
|||||||
}
|
}
|
||||||
|
|
||||||
public suspend fun respondMessage(message: DeviceMessage): DeviceMessage = try {
|
public suspend fun respondMessage(message: DeviceMessage): DeviceMessage = try {
|
||||||
val targetName = message.target?.toName() ?: Name.EMPTY
|
val targetName = message.targetName?.toName() ?: Name.EMPTY
|
||||||
val device = hub[targetName] ?: error("The device with name $targetName not found in $hub")
|
val device = hub[targetName] ?: error("The device with name $targetName not found in $hub")
|
||||||
DeviceController.respondMessage(device, targetName.toString(), message)
|
DeviceController.respondMessage(device, targetName.toString(), message)
|
||||||
} catch (ex: Exception) {
|
} catch (ex: Exception) {
|
||||||
|
@ -48,6 +48,9 @@ public fun DeviceProperty.int(): ReadWriteProperty<Any?, Int> = convert(MetaConv
|
|||||||
public fun ReadOnlyDeviceProperty.string(): ReadOnlyProperty<Any?, String> = convert(MetaConverter.string)
|
public fun ReadOnlyDeviceProperty.string(): ReadOnlyProperty<Any?, String> = convert(MetaConverter.string)
|
||||||
public fun DeviceProperty.string(): ReadWriteProperty<Any?, String> = convert(MetaConverter.string)
|
public fun DeviceProperty.string(): ReadWriteProperty<Any?, String> = convert(MetaConverter.string)
|
||||||
|
|
||||||
|
public fun ReadOnlyDeviceProperty.boolean(): ReadOnlyProperty<Any?, Boolean> = convert(MetaConverter.boolean)
|
||||||
|
public fun DeviceProperty.boolean(): ReadWriteProperty<Any?, Boolean> = convert(MetaConverter.boolean)
|
||||||
|
|
||||||
//TODO to be moved to DF
|
//TODO to be moved to DF
|
||||||
private object DurationConverter : MetaConverter<Duration> {
|
private object DurationConverter : MetaConverter<Duration> {
|
||||||
override fun itemToObject(item: MetaItem<*>): Duration = when (item) {
|
override fun itemToObject(item: MetaItem<*>): Duration = when (item) {
|
||||||
|
@ -3,22 +3,21 @@ package hep.dataforge.control.ports
|
|||||||
import hep.dataforge.context.Context
|
import hep.dataforge.context.Context
|
||||||
import hep.dataforge.context.ContextAware
|
import hep.dataforge.context.ContextAware
|
||||||
import hep.dataforge.context.Factory
|
import hep.dataforge.context.Factory
|
||||||
|
import hep.dataforge.control.api.Socket
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.coroutines.channels.Channel
|
import kotlinx.coroutines.channels.Channel
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.receiveAsFlow
|
import kotlinx.coroutines.flow.receiveAsFlow
|
||||||
import kotlinx.io.Closeable
|
|
||||||
import kotlin.coroutines.CoroutineContext
|
import kotlin.coroutines.CoroutineContext
|
||||||
|
|
||||||
public interface Port: Closeable, ContextAware {
|
public interface Port : ContextAware, Socket<ByteArray>
|
||||||
public suspend fun send(data: ByteArray)
|
|
||||||
public suspend fun receiving(): Flow<ByteArray>
|
|
||||||
public fun isOpen(): Boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
public typealias PortFactory = Factory<Port>
|
public typealias PortFactory = Factory<Port>
|
||||||
|
|
||||||
public abstract class AbstractPort(override val context: Context, coroutineContext: CoroutineContext = context.coroutineContext) : Port {
|
public abstract class AbstractPort(
|
||||||
|
override val context: Context,
|
||||||
|
coroutineContext: CoroutineContext = context.coroutineContext,
|
||||||
|
) : Port {
|
||||||
|
|
||||||
protected val scope: CoroutineScope = CoroutineScope(coroutineContext + SupervisorJob(coroutineContext[Job]))
|
protected val scope: CoroutineScope = CoroutineScope(coroutineContext + SupervisorJob(coroutineContext[Job]))
|
||||||
|
|
||||||
@ -70,7 +69,7 @@ public abstract class AbstractPort(override val context: Context, coroutineConte
|
|||||||
* In order to form phrases some condition should used on top of it.
|
* In order to form phrases some condition should used on top of it.
|
||||||
* For example [delimitedIncoming] generates phrases with fixed delimiter.
|
* For example [delimitedIncoming] generates phrases with fixed delimiter.
|
||||||
*/
|
*/
|
||||||
override suspend fun receiving(): Flow<ByteArray> {
|
override fun receiving(): Flow<ByteArray> {
|
||||||
return incoming.receiveAsFlow()
|
return incoming.receiveAsFlow()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -43,7 +43,7 @@ public class PortProxy(override val context: Context = Global, public val factor
|
|||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
override suspend fun receiving(): Flow<ByteArray> = channelFlow {
|
override fun receiving(): Flow<ByteArray> = channelFlow {
|
||||||
while (isActive) {
|
while (isActive) {
|
||||||
try {
|
try {
|
||||||
//recreate port and Flow on cancel
|
//recreate port and Flow on cancel
|
||||||
|
@ -39,7 +39,7 @@ public fun Flow<ByteArray>.withDelimiter(delimiter: ByteArray, expectedMessageSi
|
|||||||
* Transform byte fragments into utf-8 phrases using utf-8 delimiter
|
* Transform byte fragments into utf-8 phrases using utf-8 delimiter
|
||||||
*/
|
*/
|
||||||
public fun Flow<ByteArray>.withDelimiter(delimiter: String, expectedMessageSize: Int = 32): Flow<String> {
|
public fun Flow<ByteArray>.withDelimiter(delimiter: String, expectedMessageSize: Int = 32): Flow<String> {
|
||||||
return withDelimiter(delimiter.encodeToByteArray()).map { it.decodeToString() }
|
return withDelimiter(delimiter.encodeToByteArray(),expectedMessageSize).map { it.decodeToString() }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -8,7 +8,6 @@ import hep.dataforge.control.controllers.DeviceController.Companion.GET_PROPERTY
|
|||||||
import hep.dataforge.control.controllers.DeviceController.Companion.SET_PROPERTY_ACTION
|
import hep.dataforge.control.controllers.DeviceController.Companion.SET_PROPERTY_ACTION
|
||||||
import hep.dataforge.control.controllers.DeviceManager
|
import hep.dataforge.control.controllers.DeviceManager
|
||||||
import hep.dataforge.control.controllers.DeviceMessage
|
import hep.dataforge.control.controllers.DeviceMessage
|
||||||
import hep.dataforge.control.controllers.data
|
|
||||||
import hep.dataforge.control.controllers.respondMessage
|
import hep.dataforge.control.controllers.respondMessage
|
||||||
import hep.dataforge.meta.toJson
|
import hep.dataforge.meta.toJson
|
||||||
import hep.dataforge.meta.toMeta
|
import hep.dataforge.meta.toMeta
|
||||||
@ -47,7 +46,7 @@ import kotlinx.serialization.json.put
|
|||||||
public fun CoroutineScope.startDeviceServer(
|
public fun CoroutineScope.startDeviceServer(
|
||||||
manager: DeviceManager,
|
manager: DeviceManager,
|
||||||
port: Int = 8111,
|
port: Int = 8111,
|
||||||
host: String = "localhost"
|
host: String = "localhost",
|
||||||
): ApplicationEngine {
|
): ApplicationEngine {
|
||||||
|
|
||||||
return this.embeddedServer(CIO, port, host) {
|
return this.embeddedServer(CIO, port, host) {
|
||||||
@ -80,7 +79,7 @@ public const val WEB_SERVER_TARGET: String = "@webServer"
|
|||||||
public fun Application.deviceModule(
|
public fun Application.deviceModule(
|
||||||
manager: DeviceManager,
|
manager: DeviceManager,
|
||||||
deviceNames: Collection<String> = manager.devices.keys.map { it.toString() },
|
deviceNames: Collection<String> = manager.devices.keys.map { it.toString() },
|
||||||
route: String = "/"
|
route: String = "/",
|
||||||
) {
|
) {
|
||||||
// val controllers = deviceNames.associateWith { name ->
|
// val controllers = deviceNames.associateWith { name ->
|
||||||
// val device = manager.devices[name.toName()]
|
// val device = manager.devices[name.toName()]
|
||||||
@ -115,7 +114,8 @@ public fun Application.deviceModule(
|
|||||||
+"Device server dashboard"
|
+"Device server dashboard"
|
||||||
}
|
}
|
||||||
deviceNames.forEach { deviceName ->
|
deviceNames.forEach { deviceName ->
|
||||||
val device = manager[deviceName] ?: error("The device with name $deviceName not found in $manager")
|
val device =
|
||||||
|
manager[deviceName] ?: error("The device with name $deviceName not found in $manager")
|
||||||
div {
|
div {
|
||||||
id = deviceName
|
id = deviceName
|
||||||
h2 { +deviceName }
|
h2 { +deviceName }
|
||||||
@ -203,12 +203,10 @@ public fun Application.deviceModule(
|
|||||||
val target: String by call.parameters
|
val target: String by call.parameters
|
||||||
val property: String by call.parameters
|
val property: String by call.parameters
|
||||||
val request = DeviceMessage {
|
val request = DeviceMessage {
|
||||||
type = GET_PROPERTY_ACTION
|
action = GET_PROPERTY_ACTION
|
||||||
source = WEB_SERVER_TARGET
|
sourceName = WEB_SERVER_TARGET
|
||||||
this.target = target
|
this.targetName = target
|
||||||
data {
|
key = property
|
||||||
name = property
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val response = manager.respondMessage(request)
|
val response = manager.respondMessage(request)
|
||||||
@ -221,13 +219,12 @@ public fun Application.deviceModule(
|
|||||||
val json = Json.parseToJsonElement(body)
|
val json = Json.parseToJsonElement(body)
|
||||||
|
|
||||||
val request = DeviceMessage {
|
val request = DeviceMessage {
|
||||||
type = SET_PROPERTY_ACTION
|
action = SET_PROPERTY_ACTION
|
||||||
source = WEB_SERVER_TARGET
|
sourceName = WEB_SERVER_TARGET
|
||||||
this.target = target
|
this.targetName = target
|
||||||
data {
|
key = property
|
||||||
name = property
|
value = json.toMetaItem()
|
||||||
value = json.toMetaItem()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val response = manager.respondMessage(request)
|
val response = manager.respondMessage(request)
|
||||||
|
@ -4,6 +4,8 @@ import hep.dataforge.context.Context
|
|||||||
import hep.dataforge.control.api.DeviceHub
|
import hep.dataforge.control.api.DeviceHub
|
||||||
import hep.dataforge.control.api.PropertyDescriptor
|
import hep.dataforge.control.api.PropertyDescriptor
|
||||||
import hep.dataforge.control.base.*
|
import hep.dataforge.control.base.*
|
||||||
|
import hep.dataforge.control.controllers.boolean
|
||||||
|
import hep.dataforge.control.controllers.double
|
||||||
import hep.dataforge.control.controllers.duration
|
import hep.dataforge.control.controllers.duration
|
||||||
import hep.dataforge.control.ports.Port
|
import hep.dataforge.control.ports.Port
|
||||||
import hep.dataforge.control.ports.PortProxy
|
import hep.dataforge.control.ports.PortProxy
|
||||||
@ -12,15 +14,12 @@ import hep.dataforge.control.ports.withDelimiter
|
|||||||
import hep.dataforge.meta.MetaItem
|
import hep.dataforge.meta.MetaItem
|
||||||
import hep.dataforge.names.NameToken
|
import hep.dataforge.names.NameToken
|
||||||
import hep.dataforge.values.Null
|
import hep.dataforge.values.Null
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.coroutines.Job
|
|
||||||
import kotlinx.coroutines.SupervisorJob
|
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.flow.takeWhile
|
import kotlinx.coroutines.flow.takeWhile
|
||||||
import kotlinx.coroutines.flow.toList
|
import kotlinx.coroutines.flow.toList
|
||||||
import kotlinx.coroutines.sync.Mutex
|
import kotlinx.coroutines.sync.Mutex
|
||||||
import kotlinx.coroutines.sync.withLock
|
import kotlinx.coroutines.sync.withLock
|
||||||
import kotlinx.coroutines.withTimeout
|
|
||||||
import kotlin.time.Duration
|
import kotlin.time.Duration
|
||||||
|
|
||||||
|
|
||||||
@ -48,7 +47,7 @@ public class PiMotionMasterDevice(
|
|||||||
|
|
||||||
private val mutex = Mutex()
|
private val mutex = Mutex()
|
||||||
|
|
||||||
private suspend fun dispatchError(errorCode: Int){
|
private suspend fun dispatchError(errorCode: Int) {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -62,7 +61,7 @@ public class PiMotionMasterDevice(
|
|||||||
connector.send(stringToSend)
|
connector.send(stringToSend)
|
||||||
}
|
}
|
||||||
|
|
||||||
public suspend fun getErrorCode(): Int = mutex.withLock{
|
public suspend fun getErrorCode(): Int = mutex.withLock {
|
||||||
withTimeout(timeoutValue) {
|
withTimeout(timeoutValue) {
|
||||||
sendCommandInternal("ERR?")
|
sendCommandInternal("ERR?")
|
||||||
val errorString = connector.receiving().withDelimiter("\n").first()
|
val errorString = connector.receiving().withDelimiter("\n").first()
|
||||||
@ -81,7 +80,7 @@ public class PiMotionMasterDevice(
|
|||||||
val phrases = connector.receiving().withDelimiter("\n")
|
val phrases = connector.receiving().withDelimiter("\n")
|
||||||
phrases.takeWhile { it.endsWith(" \n") }.toList() + phrases.first()
|
phrases.takeWhile { it.endsWith(" \n") }.toList() + phrases.first()
|
||||||
}
|
}
|
||||||
} catch (ex: Throwable){
|
} catch (ex: Throwable) {
|
||||||
logger.warn { "Error during PIMotionMaster request. Requesting error code." }
|
logger.warn { "Error during PIMotionMaster request. Requesting error code." }
|
||||||
val errorCode = getErrorCode()
|
val errorCode = getErrorCode()
|
||||||
dispatchError(errorCode)
|
dispatchError(errorCode)
|
||||||
@ -185,10 +184,25 @@ public class PiMotionMasterDevice(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
public val reference: ReadOnlyDeviceProperty by readingBoolean(
|
||||||
|
descriptorBuilder = {
|
||||||
|
info = "Get Referencing Result"
|
||||||
|
},
|
||||||
|
getter = {
|
||||||
|
readAxisBoolean("FRF?")
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
val moveToReference by acting {
|
||||||
|
send("FRF", axisId)
|
||||||
|
}
|
||||||
|
|
||||||
public val position: DeviceProperty by axisNumberProperty("POS") {
|
public val position: DeviceProperty by axisNumberProperty("POS") {
|
||||||
info = "The current axis position."
|
info = "The current axis position."
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var positionValue by position.double()
|
||||||
|
|
||||||
public val openLoopTarget: DeviceProperty by axisNumberProperty("OMA") {
|
public val openLoopTarget: DeviceProperty by axisNumberProperty("OMA") {
|
||||||
info = "Position for open-loop operation."
|
info = "Position for open-loop operation."
|
||||||
}
|
}
|
||||||
@ -197,12 +211,18 @@ public class PiMotionMasterDevice(
|
|||||||
info = "Servo closed loop mode"
|
info = "Servo closed loop mode"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var closedLoopValue by closedLoop.boolean()
|
||||||
|
|
||||||
public val velocity: DeviceProperty by axisNumberProperty("VEL") {
|
public val velocity: DeviceProperty by axisNumberProperty("VEL") {
|
||||||
info = "Velocity value for closed-loop operation"
|
info = "Velocity value for closed-loop operation"
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override val devices: Map<NameToken, Axis> = axes.associate { NameToken(it) to Axis(it) }
|
override val devices: Map<NameToken, Axis> = axes.associate { NameToken(it) to Axis(it) }
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
val axes: Map<String, Axis> get() = devices.mapKeys { it.toString() }
|
||||||
|
|
||||||
}
|
}
|
@ -1,51 +1,60 @@
|
|||||||
package ru.mipt.npm.devices.pimotionmaster
|
package ru.mipt.npm.devices.pimotionmaster
|
||||||
|
|
||||||
import hep.dataforge.context.Context
|
import hep.dataforge.context.Context
|
||||||
|
import hep.dataforge.control.api.Socket
|
||||||
import hep.dataforge.control.ports.AbstractPort
|
import hep.dataforge.control.ports.AbstractPort
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import hep.dataforge.control.ports.withDelimiter
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.coroutines.channels.Channel
|
import kotlinx.coroutines.channels.Channel
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.flow.*
|
||||||
import kotlinx.coroutines.flow.Flow
|
|
||||||
import kotlinx.coroutines.flow.collect
|
|
||||||
import kotlinx.coroutines.flow.receiveAsFlow
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
import kotlin.time.Duration
|
import kotlin.time.Duration
|
||||||
|
|
||||||
public abstract class VirtualDevice {
|
abstract class VirtualDevice(val scope: CoroutineScope) : Socket<ByteArray> {
|
||||||
protected abstract val scope: CoroutineScope
|
|
||||||
|
|
||||||
public abstract suspend fun evaluateRequest(request: ByteArray)
|
protected abstract suspend fun evaluateRequest(request: ByteArray)
|
||||||
|
|
||||||
private val toSend = Channel<ByteArray>(100)
|
protected open fun Flow<ByteArray>.transformRequests(): Flow<ByteArray> = this
|
||||||
|
|
||||||
public val responses: Flow<ByteArray> get() = toSend.receiveAsFlow()
|
private val toReceive = Channel<ByteArray>(100)
|
||||||
|
private val toRespond = Channel<ByteArray>(100)
|
||||||
|
|
||||||
protected suspend fun send(response: ByteArray) {
|
private val receiveJob: Job = toReceive.consumeAsFlow().onEach {
|
||||||
toSend.send(response)
|
evaluateRequest(it)
|
||||||
|
}.catch {
|
||||||
|
it.printStackTrace()
|
||||||
|
}.launchIn(scope)
|
||||||
|
|
||||||
|
|
||||||
|
override suspend fun send(data: ByteArray) {
|
||||||
|
toReceive.send(data)
|
||||||
}
|
}
|
||||||
//
|
|
||||||
// protected suspend fun respond(response: String){
|
protected suspend fun respond(response: ByteArray) {
|
||||||
// respond(response.encodeToByteArray())
|
toRespond.send(response)
|
||||||
// }
|
}
|
||||||
|
|
||||||
|
override fun receiving(): Flow<ByteArray> = toRespond.receiveAsFlow()
|
||||||
|
|
||||||
protected fun respondInFuture(delay: Duration, response: suspend () -> ByteArray): Job = scope.launch {
|
protected fun respondInFuture(delay: Duration, response: suspend () -> ByteArray): Job = scope.launch {
|
||||||
delay(delay)
|
delay(delay)
|
||||||
send(response())
|
respond(response())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun isOpen(): Boolean = scope.isActive
|
||||||
|
|
||||||
|
override fun close() = scope.cancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
public class VirtualPort(private val device: VirtualDevice, context: Context) : AbstractPort(context) {
|
class VirtualPort(private val device: VirtualDevice, context: Context) : AbstractPort(context) {
|
||||||
|
|
||||||
|
private val respondJob = device.receiving().onEach(::receive).catch {
|
||||||
|
it.printStackTrace()
|
||||||
|
}.launchIn(scope)
|
||||||
|
|
||||||
private val respondJob = scope.launch {
|
|
||||||
device.responses.collect {
|
|
||||||
receive(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun write(data: ByteArray) {
|
override suspend fun write(data: ByteArray) {
|
||||||
device.evaluateRequest(data)
|
device.send(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun close() {
|
override fun close() {
|
||||||
@ -55,13 +64,13 @@ public class VirtualPort(private val device: VirtualDevice, context: Context) :
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class PiMotionMasterVirtualDevice(override val scope: CoroutineScope, axisIds: List<String>) : VirtualDevice() {
|
class PiMotionMasterVirtualDevice(scope: CoroutineScope, axisIds: List<String>) : VirtualDevice(scope) {
|
||||||
|
|
||||||
init {
|
init {
|
||||||
//add asynchronous send logic here
|
//add asynchronous send logic here
|
||||||
}
|
}
|
||||||
|
|
||||||
private val axisID = "0"
|
override fun Flow<ByteArray>.transformRequests(): Flow<ByteArray> = withDelimiter("\n".toByteArray())
|
||||||
|
|
||||||
private var errorCode: Int = 0
|
private var errorCode: Int = 0
|
||||||
|
|
||||||
@ -116,7 +125,7 @@ class PiMotionMasterVirtualDevice(override val scope: CoroutineScope, axisIds: L
|
|||||||
|
|
||||||
|
|
||||||
private fun respond(str: String) = scope.launch {
|
private fun respond(str: String) = scope.launch {
|
||||||
send((str + "\n").encodeToByteArray())
|
respond((str + "\n").encodeToByteArray())
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun respondForAllAxis(axisIds: List<String>, extract: VirtualAxisState.(index: String) -> Any) {
|
private fun respondForAllAxis(axisIds: List<String>, extract: VirtualAxisState.(index: String) -> Any) {
|
||||||
|
@ -28,21 +28,23 @@ fun CoroutineScope.launchPiDebugServer(port: Int, virtualPort: Port): Job = laun
|
|||||||
server.accept()
|
server.accept()
|
||||||
} catch (ex: Exception) {
|
} catch (ex: Exception) {
|
||||||
server.close()
|
server.close()
|
||||||
|
ex.printStackTrace()
|
||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
|
|
||||||
launch {
|
|
||||||
println("Socket accepted: ${socket.remoteAddress}")
|
|
||||||
|
|
||||||
try {
|
println("Socket accepted: ${socket.remoteAddress}")
|
||||||
|
supervisorScope {
|
||||||
|
socket.use { socket ->
|
||||||
val input = socket.openReadChannel()
|
val input = socket.openReadChannel()
|
||||||
val output = socket.openWriteChannel(autoFlush = true)
|
val output = socket.openWriteChannel(autoFlush = true)
|
||||||
|
|
||||||
val buffer = ByteBuffer.allocate(1024)
|
val buffer = ByteBuffer.allocate(1024)
|
||||||
launch {
|
launch {
|
||||||
virtualPort.receiving().collect {
|
virtualPort.receiving().collect {
|
||||||
println("Sending: ${it.decodeToString()}")
|
//println("Sending: ${it.decodeToString()}")
|
||||||
output.writeAvailable(it)
|
output.writeAvailable(it)
|
||||||
|
output.flush()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
while (isActive) {
|
while (isActive) {
|
||||||
@ -51,14 +53,10 @@ fun CoroutineScope.launchPiDebugServer(port: Int, virtualPort: Port): Job = laun
|
|||||||
if (read > 0) {
|
if (read > 0) {
|
||||||
buffer.flip()
|
buffer.flip()
|
||||||
val array = buffer.moveToByteArray()
|
val array = buffer.moveToByteArray()
|
||||||
println("Received: ${array.decodeToString()}")
|
//println("Received: ${array.decodeToString()}")
|
||||||
virtualPort.send(array)
|
virtualPort.send(array)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (ex: Exception) {
|
|
||||||
cancel()
|
|
||||||
} finally {
|
|
||||||
socket.close()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -66,7 +64,7 @@ fun CoroutineScope.launchPiDebugServer(port: Int, virtualPort: Port): Job = laun
|
|||||||
|
|
||||||
fun main() {
|
fun main() {
|
||||||
val port = 10024
|
val port = 10024
|
||||||
val virtualDevice = PiMotionMasterVirtualDevice(Global, listOf("1","2"))
|
val virtualDevice = PiMotionMasterVirtualDevice(Global, listOf("1", "2"))
|
||||||
val virtualPort = VirtualPort(virtualDevice, Global)
|
val virtualPort = VirtualPort(virtualDevice, Global)
|
||||||
runBlocking(Dispatchers.Default) {
|
runBlocking(Dispatchers.Default) {
|
||||||
val serverJob = launchPiDebugServer(port, virtualPort)
|
val serverJob = launchPiDebugServer(port, virtualPort)
|
||||||
|
Loading…
Reference in New Issue
Block a user