Skip to content

Commit

Permalink
Merge pull request OSGP#74 from OSGP/feature/FDP-2551-command-result-…
Browse files Browse the repository at this point in the history
…hander-refactoring

Feature/fdp 2551 command result handler refactoring
  • Loading branch information
smvdheijden authored Nov 22, 2024
2 parents 7fe279e + 11fcf61 commit 51b4cb1
Show file tree
Hide file tree
Showing 33 changed files with 1,256 additions and 426 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,15 @@ import org.springframework.kafka.test.utils.KafkaTestUtils
import org.springframework.test.annotation.DirtiesContext

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT)
@EmbeddedKafka(topics = ["\${kafka.producers.command-feedback.topic}"])
@EmbeddedKafka(
topics =
[
"\${kafka.consumers.command.topic}",
"\${kafka.consumers.pre-shared-key.topic}",
"\${kafka.producers.command-feedback.topic}",
"\${kafka.producers.device-message.topic}"
]
)
@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD)
class CoapMessageHandlingTest {
companion object {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,23 +41,13 @@ class Command(

fun cancel() = apply { status = CommandStatus.CANCELLED }

enum class CommandType(
val downlink: String,
val urcsSuccess: List<String>,
val urcsError: List<String>,
val needsCommandValue: Boolean = false
) {
PSK("PSK", listOf("PSK:TMP"), listOf("PSK:DLER", "PSK:HSER")),
PSK_SET("PSK:SET", listOf("PSK:SET"), listOf("PSK:DLER", "PSK:HSER", "PSK:EQER")),
REBOOT("CMD:REBOOT", listOf("INIT", "WDR"), listOf()),
RSP("CMD:RSP", listOf("CMD:RSP"), listOf("DLER")),
RSP2("CMD:RSP2", listOf("CMD:RSP2"), listOf("DLER")),
FIRMWARE(
"OTA",
listOf("OTA:SUC"),
listOf("OTA:CSER", "OTA:HSER", "OTA:RST", "OTA:SWNA", "OTA:FLER"),
needsCommandValue = true
),
enum class CommandType(val downlink: String, val needsCommandValue: Boolean = false) {
PSK("PSK"),
PSK_SET("PSK:SET"),
REBOOT("CMD:REBOOT"),
RSP("CMD:RSP"),
RSP2("CMD:RSP2"),
FIRMWARE("OTA", needsCommandValue = true),
}

enum class CommandStatus {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// SPDX-FileCopyrightText: Copyright Contributors to the GXF project
//
// SPDX-License-Identifier: Apache-2.0
package org.gxf.crestdeviceservice.command.exception

class NoCommandResultHandlerForCommandTypeException(message: String) : Exception(message)
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
// SPDX-FileCopyrightText: Copyright Contributors to the GXF project
//
// SPDX-License-Identifier: Apache-2.0
package org.gxf.crestdeviceservice.command.resulthandler

import com.fasterxml.jackson.databind.JsonNode
import io.github.oshai.kotlinlogging.KotlinLogging
import org.gxf.crestdeviceservice.command.entity.Command
import org.gxf.crestdeviceservice.command.entity.Command.CommandType
import org.gxf.crestdeviceservice.command.service.CommandFeedbackService
import org.gxf.crestdeviceservice.command.service.CommandService
import org.gxf.crestdeviceservice.model.ErrorUrc.Companion.getMessageFromCode

abstract class CommandResultHandler(
private val commandService: CommandService,
private val commandFeedbackService: CommandFeedbackService
) {
private val logger = KotlinLogging.logger {}

abstract val supportedCommandType: CommandType

abstract fun hasSucceeded(deviceId: String, body: JsonNode): Boolean

abstract fun hasFailed(deviceId: String, body: JsonNode): Boolean

fun handleSuccess(command: Command) {
logger.info { "Command ${command.type} succeeded for device with id ${command.deviceId}." }

handleCommandSpecificSuccess(command)

logger.debug { "Saving command and sending feedback to Maki." }
val successfulCommand = commandService.saveCommand(command.finish())
commandFeedbackService.sendSuccessFeedback(successfulCommand)
}

/** Override this method when custom success actions are needed. */
open fun handleCommandSpecificSuccess(command: Command) {
logger.debug {
"Command ${command.type} for device with id ${command.deviceId} does not require specific success handling."
}
}

fun handleFailure(command: Command, body: JsonNode) {
logger.info { "Command ${command.type} failed for device with id ${command.deviceId}." }

handleCommandSpecificFailure(command, body)

val failedCommand = commandService.saveCommand(command.fail())
val errorMessages = body.urcs().joinToString(". ") { urc -> getMessageFromCode(urc) }
commandFeedbackService.sendErrorFeedback(failedCommand, "Command failed. Error(s): $errorMessages.")
}

/** Override this method when command specific failure actions are needed */
open fun handleCommandSpecificFailure(command: Command, body: JsonNode) {
logger.debug {
"Command ${command.type} for device with id ${command.deviceId} does not require specific failure handling."
}
}

fun handleStillInProgress(command: Command) {
logger.info { "Command ${command.type} still in progress for device with id ${command.deviceId}." }
}

companion object {
private const val URC_FIELD = "URC"
private const val DL_FIELD = "DL"

fun JsonNode.urcs(): List<String> = this[URC_FIELD].filter { it.isTextual }.map { it.asText() }

fun JsonNode.downlinks(): List<String> =
this[URC_FIELD].first { it.isObject }[DL_FIELD].asText().replace("!", "").split(";")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// SPDX-FileCopyrightText: Copyright Contributors to the GXF project
//
// SPDX-License-Identifier: Apache-2.0
package org.gxf.crestdeviceservice.command.resulthandler

import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration

@Configuration
class CommandResultHandlerConfig {

@Bean
fun commandResultHandlersByType(commandResultHandlers: List<CommandResultHandler>) =
commandResultHandlers.associateBy { it.supportedCommandType }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// SPDX-FileCopyrightText: Copyright Contributors to the GXF project
//
// SPDX-License-Identifier: Apache-2.0
package org.gxf.crestdeviceservice.command.resulthandler

import com.fasterxml.jackson.databind.JsonNode
import org.gxf.crestdeviceservice.command.entity.Command.CommandType
import org.gxf.crestdeviceservice.command.service.CommandFeedbackService
import org.gxf.crestdeviceservice.command.service.CommandService
import org.springframework.stereotype.Component

@Component
class FirmwareCommandResultHandler(
val commandService: CommandService,
val commandFeedbackService: CommandFeedbackService
) : CommandResultHandler(commandService, commandFeedbackService) {

private val succesUrc = "OTA:SUC"
private val errorUrcs = listOf("OTA:CSER", "OTA:HSER", "OTA:RST", "OTA:SWNA", "OTA:FLER")

override val supportedCommandType = CommandType.FIRMWARE

override fun hasSucceeded(deviceId: String, body: JsonNode) = succesUrc in body.urcs()

override fun hasFailed(deviceId: String, body: JsonNode) = body.urcs().any { it in errorUrcs }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// SPDX-FileCopyrightText: Copyright Contributors to the GXF project
//
// SPDX-License-Identifier: Apache-2.0
package org.gxf.crestdeviceservice.command.resulthandler

import com.fasterxml.jackson.databind.JsonNode
import org.gxf.crestdeviceservice.command.entity.Command.CommandType
import org.gxf.crestdeviceservice.command.service.CommandFeedbackService
import org.gxf.crestdeviceservice.command.service.CommandService
import org.springframework.stereotype.Component

@Component
class PskCommandResultHandler(commandService: CommandService, commandFeedbackService: CommandFeedbackService) :
CommandResultHandler(commandService, commandFeedbackService) {

private val succesUrc = "PSK:TMP"
private val errorUrcs = listOf("PSK:DLER", "PSK:HSER")

override val supportedCommandType = CommandType.PSK

override fun hasSucceeded(deviceId: String, body: JsonNode) = succesUrc in body.urcs()

override fun hasFailed(deviceId: String, body: JsonNode) = body.urcs().any { it in errorUrcs }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// SPDX-FileCopyrightText: Copyright Contributors to the GXF project
//
// SPDX-License-Identifier: Apache-2.0
package org.gxf.crestdeviceservice.command.resulthandler

import com.fasterxml.jackson.databind.JsonNode
import io.github.oshai.kotlinlogging.KotlinLogging
import org.gxf.crestdeviceservice.command.entity.Command
import org.gxf.crestdeviceservice.command.entity.Command.CommandType
import org.gxf.crestdeviceservice.command.service.CommandFeedbackService
import org.gxf.crestdeviceservice.command.service.CommandService
import org.gxf.crestdeviceservice.psk.service.PskService
import org.springframework.stereotype.Component

@Component
class PskSetCommandResultHandler(
val pskService: PskService,
val commandService: CommandService,
val commandFeedbackService: CommandFeedbackService
) : CommandResultHandler(commandService, commandFeedbackService) {

private val logger = KotlinLogging.logger {}

private val succesUrc = "PSK:SET"
private val errorUrcs = listOf("PSK:DLER", "PSK:HSER", "PSK:EQER")

override val supportedCommandType = CommandType.PSK_SET

override fun hasSucceeded(deviceId: String, body: JsonNode) = succesUrc in body.urcs()

override fun hasFailed(deviceId: String, body: JsonNode) = body.urcs().any { it in errorUrcs }

override fun handleCommandSpecificSuccess(command: Command) {
logger.info { "PSK SET command succeeded: Changing active key for device ${command.deviceId}" }
pskService.changeActiveKey(command.deviceId)
}

override fun handleCommandSpecificFailure(command: Command, body: JsonNode) {
logger.info { "PSK SET command failed: Setting pending key as invalid for device ${command.deviceId}" }
pskService.setPendingKeyAsInvalid(command.deviceId)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// SPDX-FileCopyrightText: Copyright Contributors to the GXF project
//
// SPDX-License-Identifier: Apache-2.0
package org.gxf.crestdeviceservice.command.resulthandler

import com.fasterxml.jackson.databind.JsonNode
import org.gxf.crestdeviceservice.command.entity.Command.CommandType
import org.gxf.crestdeviceservice.command.service.CommandFeedbackService
import org.gxf.crestdeviceservice.command.service.CommandService
import org.springframework.stereotype.Component

@Component
class RebootCommandResultHandler(commandService: CommandService, commandFeedbackService: CommandFeedbackService) :
CommandResultHandler(commandService, commandFeedbackService) {

private val succesUrcs = listOf("INIT", "WDR")

override val supportedCommandType = CommandType.REBOOT

override fun hasSucceeded(deviceId: String, body: JsonNode) = body.urcs().containsAll(succesUrcs)

override fun hasFailed(deviceId: String, body: JsonNode) = false
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// SPDX-FileCopyrightText: Copyright Contributors to the GXF project
//
// SPDX-License-Identifier: Apache-2.0
package org.gxf.crestdeviceservice.command.resulthandler

import org.gxf.crestdeviceservice.command.entity.Command.CommandType
import org.gxf.crestdeviceservice.command.service.CommandFeedbackService
import org.gxf.crestdeviceservice.command.service.CommandService
import org.springframework.stereotype.Component

@Component
class Rsp2CommandResultHandler(commandService: CommandService, commandFeedbackService: CommandFeedbackService) :
RspCommandResultHandler(commandService, commandFeedbackService) {

override val confirmationDownlinkInUrc = "CMD:RSP2"
override val errorUrc = "RSP2:DLER"

override val supportedCommandType = CommandType.RSP2
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// SPDX-FileCopyrightText: Copyright Contributors to the GXF project
//
// SPDX-License-Identifier: Apache-2.0
package org.gxf.crestdeviceservice.command.resulthandler

import com.fasterxml.jackson.databind.JsonNode
import org.gxf.crestdeviceservice.command.entity.Command.CommandType
import org.gxf.crestdeviceservice.command.service.CommandFeedbackService
import org.gxf.crestdeviceservice.command.service.CommandService
import org.springframework.stereotype.Component

@Component
class RspCommandResultHandler(commandService: CommandService, commandFeedbackService: CommandFeedbackService) :
CommandResultHandler(commandService, commandFeedbackService) {

val confirmationDownlinkInUrc = "CMD:RSP"
val errorUrc = "RSP:DLER"

override val supportedCommandType = CommandType.RSP

override fun hasSucceeded(deviceId: String, body: JsonNode) =
confirmationDownlinkInUrc in body.downlinks() && errorUrc !in body.urcs()

override fun hasFailed(deviceId: String, body: JsonNode) = errorUrc in body.urcs()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// SPDX-FileCopyrightText: Copyright Contributors to the GXF project
//
// SPDX-License-Identifier: Apache-2.0
package org.gxf.crestdeviceservice.command.service

import com.fasterxml.jackson.databind.JsonNode
import io.github.oshai.kotlinlogging.KotlinLogging
import org.gxf.crestdeviceservice.command.entity.Command
import org.gxf.crestdeviceservice.command.exception.NoCommandResultHandlerForCommandTypeException
import org.gxf.crestdeviceservice.command.resulthandler.CommandResultHandler
import org.springframework.stereotype.Service

@Service
class CommandResultService(
private val commandService: CommandService,
private val commandResultHandlersByType: Map<Command.CommandType, CommandResultHandler>
) {
private val logger = KotlinLogging.logger {}

fun handleMessage(deviceId: String, body: JsonNode) {
val commandsInProgress = commandService.getAllCommandsInProgressForDevice(deviceId)

commandsInProgress.forEach { checkResult(it, body) }
}

private fun checkResult(command: Command, body: JsonNode) {
logger.debug { "Checking result for pending command of type ${command.type} for device ${command.deviceId}" }

val resultHandler =
commandResultHandlersByType[command.type]
?: throw NoCommandResultHandlerForCommandTypeException(
"No command result handler for command type ${command.type}"
)

when {
resultHandler.hasSucceeded(command.deviceId, body) -> resultHandler.handleSuccess(command)
resultHandler.hasFailed(command.deviceId, body) -> resultHandler.handleFailure(command, body)
else -> resultHandler.handleStillInProgress(command)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import com.fasterxml.jackson.databind.JsonNode
import org.gxf.crestdeviceservice.command.entity.Command
import org.gxf.crestdeviceservice.command.exception.NoMatchingCommandException
import org.gxf.crestdeviceservice.command.service.CommandFeedbackService
import org.gxf.crestdeviceservice.command.service.CommandResultService
import org.gxf.crestdeviceservice.command.service.CommandService
import org.gxf.crestdeviceservice.firmware.service.FirmwareService
import org.gxf.crestdeviceservice.model.DeviceMessage
Expand All @@ -15,22 +16,22 @@ import org.springframework.stereotype.Service

@Service
class PayloadService(
private val urcService: UrcService,
private val commandResultService: CommandResultService,
private val firmwareService: FirmwareService,
private val commandService: CommandService,
private val commandFeedbackService: CommandFeedbackService,
) {
/**
* Process the payload. This includes
* - checking URCs and updating the corresponding Commands
* - checking the payload for results for the commands sent via downlinks
* - checking the payload for FMC (request for new FOTA packet)
*
* @param identity The identity of the device
* @param body The body of the device message
* @param downlink The downlink to be returned to the device, fill it here if needed
*/
fun processPayload(identity: String, body: JsonNode, downlink: Downlink) {
urcService.interpretUrcsInMessage(identity, body)
commandResultService.handleMessage(identity, body)

val deviceMessage = DeviceMessage(body)

Expand Down
Loading

0 comments on commit 51b4cb1

Please sign in to comment.