반응형
로봇 상위제어기가 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)
}
반응형
댓글