본문 바로가기
Kotlin

message 통신으로 문자열 데이터를 주고 받기

by 루에 2021. 11. 12.
반응형

로봇 상위제어기가 n개 실행되는 케이스가 발생해 제어기 간 문자열 데이터를 주고 받을 수 있는 메세지 통신 기능을 만든 적이 있다. 그걸 조금 다듬어 V2.0으로 개선했는데 해당 부분을 간략하게 기록한다.

 

먼저 구조는 다음과 같다.

1. 컨트롤러

 - 메세지를 보내는 기능(제일 많이 개선됨)

 - 요청 명령을 구분하여 정의된 함수를 찾아(generic) 실행해주는 기능

2. 요청 명령 구분자

 - 보내는 요청의 종류를 정해놓은 기능

 - 중복 요청을 막아야하는 리스트 관리

 - 그 외 구분이 필요한 그룹을 생성해서 관리하기 용이하도록 구조

 

1. 컨트롤러(많은 부분은 생략했지만...)

@Suppress("unused", "UNUSED_PARAMETER", "RedundantSuspendModifier")
class MessengerController: Controller() {
    annotation class Orders(val id: Order)
    private val functions: MutableMap<Order, KFunction<*>> = mutableMapOf()
    private val separator = "<sep>" // 새로운 데이터 구분자
    private val oldSeparator = "#"  // 초기의 데이터 구분자

    init {
        this::class.declaredFunctions.forEach { func ->
            func.annotations.filterIsInstance<Orders>()
                .forEach { ano ->
                    functions[ano.id] = func
            }
        }
    }

    suspend fun send(from: Short, to: Short, order: Order, vararg messages: String) {
        io.engine.client.sendMessaging(from, to, "${order.ordinal}$separator${getJoinedMessages(messages)}")
    }

    suspend fun send(to: Short, order: Order, vararg messages: String) {
        io.engine.client.sendMessaging(to, "${order.ordinal}$separator${getJoinedMessages(messages)}")
    }

    suspend fun sendAll(from: Short, order: Order, vararg messages: String) {
        io.engine.client.sendMessagingAll(from, "${order.ordinal}$separator${getJoinedMessages(messages)}")
    }

    suspend fun sendAll(order: Order, vararg messages: String) {
        io.engine.client.sendMessagingAll("${order.ordinal}$separator${getJoinedMessages(messages)}")
    }

    suspend fun receive(data: Messaging, message: String): Boolean {
        val messages = message.split(if(message.contains(separator)) separator else oldSeparator)
        return log(data, messages)?.let {
            if(isAvailable(messages[0])) call(it, messages) else false
        } ?: false
    }

    private suspend fun call(key: Order, messages: List<String>): Boolean {
        return try {
            functions[key]?.callSuspend(this, messages)
            true
        } catch (e: Exception) {
            logger.error("${key.name} 처리 중 에러 발생 - ${e.message}", e)
            false
        } finally {
            OrderUtils.workingOff()
        }
    }

    // 로그를 기록하고 처리해야할 Order 객체를 리턴한다.
    private suspend fun log(data: Messaging, messages: List<String>): Order? {
        var result: Order? = null
        var from = ""
        var to = ""
        var name = ""
        Identify.values().forEach { i ->
            if(data.from == i.from) from = i.name
            if(data.to == i.to) to = i.name
        }
        OrderUtils.commonOrders.forEach { v ->
            if(messages[0] == v.ordinal.toString()) {
                name = v.name
                result = v
            }
        }
        return result.also {
            it?.apply {
                logger.info(Tag.PACKET.Received, "1901 messaging packet : [$name] $from -> $to (${data.message})")
            }
        }
    }

    private suspend fun getJoinedMessages(messages: Array<out String>): String {
        return messages.joinToString(separator = separator)
    }

    // 현재 TP가 다른 IO 작업을 수행중인지 체크
    private suspend fun isAvailable(message: String): Boolean {
        // check working order
        return OrderUtils.hasWorkingOrderList(message)?.let { order ->
            OrderUtils.isWorking.value.not().also {
                if(it) {
                    OrderUtils.workingOn(order)
                } else {
                    send(To.WINDRSC, Order.WORKING_ON_IT, message)
                }
            }
        } ?: true
    }

    // 아래는 명령 처리 함수들
    @Orders(Order.IS_BACKDRIVE)
    suspend fun backDriveInWindows(messages: List<String>) {
        robotInfo.winIsBackDrive.value = true
    }

    @Orders(Order.WORKING_ON_IT)
    // TP가 작업중일 때의 액션. Dart-Platform에서 요청할 때 세팅한 부분의 초기화 및 로딩뷰를 닫는 액션을 넣으면 된다.
    suspend fun actionAtWorking(messages: List<String>) {
        loading.hide()
        popupView.infoMessage(template["MSG_COM_VAL_064"])
    }

    @Orders(Order.VERSION_REQ)
    suspend fun requestVersion(messages: List<String>){
        val lnxDirName = Furcation.getExecutionPath().split("/")[4]
        send(From.TP, To.WINDRSC, Order.VERSION_RES, lnxDirName, robotInfo.wciInfo.value.wciVersion)
    }

    @Orders(Order.VERSION_RES)
    suspend fun responseVersion(messages: List<String>){
        logger.debug(Tag.TASK.Status, "responseVersion() ${messages[1]}")
    }

    @Orders(Order.WCI_INSTALL_REQ)
    suspend fun requestWciInstall(messages: List<String>){
        RobotUpdate.execInstall(messages[1],true)
    }

    @Orders(Order.RESTORE_DUMP_REQ)
    suspend fun requestRestore(messages: List<String>) {
        BackupRestore.requestRemoteRestore(messages)
    }

    @Orders(Order.RESTORE_DUMP_RES)
    suspend fun responseRestore(messages: List<String>) {
        BackupRestore.responseRemoteRestore(messages)
    }

    @Orders(Order.WCI_EDIT_TASK_RES)
    suspend fun responseWCITaskOpen(messages: List<String>){
        // #AuthKey#id#name
        if(messages[1] != io.engine.client.auththenticationKey) {
            robotInfo.WCIOpendTaskId.value = messages[2].toUuid()
            robotInfo.WCIOpendTaskName.value = messages[3]
        }
    }

    @Orders(Order.ANALOG_INOUT_VALUES_REQ)
    suspend fun requestAnalogInOutValues(messages: List<String>) {
        find<StatusView>().sendAnalogOutputValues()
    }

    @Orders(Order.ANALOG_INOUT_VALUES_RES)
    suspend fun responseAnalogInOutValues(messages: List<String>) {
        find<StatusView>().afterAnalogOutputValues(messages)
    }

    @Orders(Order.POWER_OFF)
    suspend fun requestRemotePowerOff(messages: List<String>){
    }

    @Orders(Order.PERMISSION_REQ)
    suspend fun responsePermissionStatus(messages: List<String>) {
        sendAll(Order.PERMISSION_RES, io.engine.client.auththenticationKey)
    }

    @Orders(Order.PERMISSION_RES)
    suspend fun onPermissionStatus(messages: List<String>) {
    }
}

2. 명령 구분자

package dra.engine

import dra.drcf.client.UnsignedChar
import dra.extenstion.containsOr
import javafx.beans.property.SimpleBooleanProperty
import javafx.beans.property.SimpleObjectProperty

enum class Order {
    .
    .
    .
    ,IS_BACKDRIVE
    ,ANALOG_INOUT_VALUES_REQ, ANALOG_INOUT_VALUES_RES
    ,NEW_UPDATE_DECOMPRESS_REQ, NEW_UPDATE_DECOMPRESS_RES
    ,SHOW_TASK_MONITORING_POPUP, HIDE_TASK_MONITORING_POPUP
    ,REMOVE_JAR_FILE_REQ
    ,RESTORE_APP_REQ, RESTORE_APP_RES
    ;
}

/**
 * Order 관련 Util 모음
 */
object OrderUtils {
    // Win -> TP로 작업이 요청되는 타입(log export등)의 모음
    private val workingOrders: List<Order> = listOf(
    )

    val armGroupOrders: List<Order> = listOf(
    )
    // 제외되어야 하는 값들을 빼고(다른 화면에서 처리하는 것들), main에서 처리해야하는 것들의 모음
    val commonOrders: List<Order> = Order.values().filter { hasExcludeList(it).not() }.toList()

    val isWorking = SimpleBooleanProperty(false)    // TP가 작업중인지
    val workingType = SimpleObjectProperty<Order>(null) // 작업중인 것의 종류

    fun hasWorkingOrderList(order: Order): Order? {
        return workingOrders.firstOrNull { it.id == order.id }
    }

    fun hasWorkingOrderList(id: String): Order? {
        return workingOrders.firstOrNull { it.id == id }
    }

    fun hasExcludeList(value: Order): Boolean {
        return armGroupOrders.containsOr(value)
    }

    fun workingOn(id: String) {
        hasWorkingOrderList(id)?.let {
            workingType.value = it
            isWorking.value = true
        }
    }

    fun workingOn(order: Order) {
        workingType.value = order
        isWorking.value = true
    }

    fun workingOff() {
        workingType.value = null
        isWorking.value = false
    }

    fun getWorking(): Pair<Boolean, Order> {
        return Pair(isWorking.value, workingType.value)
    }

    fun getWorkingProperty(): Pair<SimpleBooleanProperty, SimpleObjectProperty<Order>> {
        return Pair(isWorking, workingType)
    }
}

enum class Identify {
    TP, DRFT, PRODUCTION_SYSTEM, MONITORING_SYSTEM, OPEN_API, WINDRSC;

    // messaging packet을 보낼 때 필요한 타입으로 변환해서 반환(더 명확하게 식별 가능)
    val from = this.ordinal.toShort()
    val to = this.ordinal.toShort()
}

/**
 * 개선판
 */
class Post {
    companion object {
        @Suppress("private") const val TP: Short                = 0
        @Suppress("private") const val DRFT: Short              = 1
        @Suppress("private") const val PRODUCTION_SYSTEM: Short = 2
        @Suppress("private") const val MONITORING_SYSTEM: Short = 3
        @Suppress("private") const val OPEN_API: Short          = 4
        @Suppress("private") const val WINDRSC: Short           = 5

        fun values(): Array<Short> {
            return arrayOf(TP, DRFT, PRODUCTION_SYSTEM, MONITORING_SYSTEM, OPEN_API, WINDRSC)
        }
    }
}

val From = Post
val To = Post

 

사용 예제

old에 비해 new에서는 보내고 받는 식별자를 더 간결하고 명확하게 구별할 수 있고

명령 구분자부터 문자열로 감싸서 하나의 파라미터로 보내야 했던 것을 파라미터만 추가하는 방식으로 개선하였다. 또한 데이터 구분자를 일일이 넣어야 했던 것을 개선하고 파라미터를 하나씩 추가하는 방식으로 개편하였다.

// old
fun SendExample() {
	client.sendMessaging(Identify.TP.to, "${Order.NEW_UPDATE_DECOMPRESS_REQ.id}#$fileName")
    client.sendMessagingAll("${Order.TASK_OPEN_STATUS_REQ.ordinal}#${io.engine.client.auththenticationKey}#${controller.taskUuid.value}")
}

// new
fun SendExample() {
	messenger.send(To.TP, Order.NEW_UPDATE_DECOMPRESS_REQ, fileName)
    messenger.sendAll(Order.TASK_OPEN_STATUS_REQ, io.engine.client.auththenticationKey, controller.taskUuid.value)
}

 

반응형

댓글