Skip to content

Commit

Permalink
FDP-2551: Refactoring
Browse files Browse the repository at this point in the history
- messages are no longer read from files, but from DeviceMessage
- Simulator state is kept across messages (but not persisted over restarts)
- Message is updated with this simulator state before sending
- Simulator emulates the capacitor; after 5 messages it takes a break

Signed-off-by: Sander Verbruggen <sander.verbruggen@alliander.com>
  • Loading branch information
sanderv committed Oct 30, 2024
1 parent 018712b commit fc83127
Show file tree
Hide file tree
Showing 13 changed files with 62 additions and 233 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import java.net.URI
import java.time.Duration
import org.eclipse.californium.scandium.dtls.cipher.CipherSuite
import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.core.io.Resource

@ConfigurationProperties(prefix = "simulator.config")
class SimulatorProperties(
Expand All @@ -16,10 +15,6 @@ class SimulatorProperties(
val pskKey: String,
val pskSecret: String,
val sleepDuration: Duration,
val scheduledMessage: Resource,
val successMessage: Resource,
val failureMessage: Resource,
val rebootSuccessMessage: Resource,
val produceValidCbor: Boolean,
val cipherSuites: List<CipherSuite>,
)
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,12 @@
// SPDX-License-Identifier: Apache-2.0
package org.gxf.crestdevicesimulator.simulator

import com.fasterxml.jackson.databind.JsonNode
import com.fasterxml.jackson.databind.ObjectMapper
import io.github.oshai.kotlinlogging.KotlinLogging
import org.gxf.crestdevicesimulator.configuration.SimulatorProperties
import org.gxf.crestdevicesimulator.simulator.data.entity.SimulatorState
import org.gxf.crestdevicesimulator.simulator.message.MessageHandler
import org.springframework.boot.CommandLineRunner
import org.springframework.core.io.Resource
import org.springframework.stereotype.Component

@Component
Expand All @@ -24,21 +22,25 @@ class Simulator(

override fun run(args: Array<String>) {
logger.info { "Simulator config started with config: $simulatorProperties" }
val message: JsonNode = createMessage(simulatorProperties.scheduledMessage)
// This simulates the device trying to send as many messages as possible before the
// capacitor depletes
val maxNumberOfMessagesInBatch = 5

// Start infinite message sending loop in separate thread
// This ensures Spring Boot can complete startup and doesn't block on the infinite loop
Thread.ofVirtual().start {
val simulatorState = SimulatorState(simulatorProperties.pskIdentity)
var numberOfMessagesInBatch = 0
while (true) {
logger.info { "Sending scheduled alarm message" }
messageHandler.sendMessage(message, simulatorState)
logger.info { "Sending device message" }
val immediateResponseRequested = messageHandler.sendMessage(simulatorState)

logger.info { "Sleeping for: ${simulatorProperties.sleepDuration}" }
Thread.sleep(simulatorProperties.sleepDuration)
if (!immediateResponseRequested || numberOfMessagesInBatch++ >= maxNumberOfMessagesInBatch) {
logger.info { "Sleeping for: ${simulatorProperties.sleepDuration}" }
Thread.sleep(simulatorProperties.sleepDuration)
numberOfMessagesInBatch = 0
}
}
}
}

fun createMessage(resource: Resource): JsonNode = mapper.readTree(resource.inputStream)
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@ import org.gxf.crestdevicesimulator.simulator.message.DeviceMessageDownlink

class SimulatorState(val deviceId: String, var fotaMessageCounter: Int = 0) {

private val urcs = mutableListOf<String>()
private val urcs = mutableListOf("INIT") // INIT = boot, will be reset for second message
private val downlinks = mutableListOf<String>()

fun getUrcListForDeviceMessage(): List<Any> = urcs + listOf(DeviceMessageDownlink(downlinks.joinToString(";")))

fun resetUrcs() {
fun resetUrc() {
urcs.clear()
downlinks.clear()
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// SPDX-FileCopyrightText: Copyright Contributors to the GXF project
//
// SPDX-License-Identifier: Apache-2.0
package org.gxf.crestdevicesimulator.simulator.message

import com.fasterxml.jackson.databind.PropertyNamingStrategies.NamingBase

class CrestNamingStrategy : NamingBase() {
override fun translate(propertyName: String?): String? {
return if (propertyName.equals("cid")) "cID" else propertyName?.uppercase()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@
// SPDX-License-Identifier: Apache-2.0
package org.gxf.crestdevicesimulator.simulator.message

import com.fasterxml.jackson.annotation.JsonProperty
import com.fasterxml.jackson.databind.PropertyNamingStrategies
import com.fasterxml.jackson.databind.annotation.JsonNaming

@JsonNaming(PropertyNamingStrategies.UpperSnakeCaseStrategy::class)
@JsonNaming(CrestNamingStrategy::class)
data class DeviceMessage(
var a: List<Int> = listOf(3, 0, 0, 0, 0, 0, 0, 0),
var bat: Int = 3758,
Expand Down Expand Up @@ -182,12 +183,8 @@ data class DeviceMessage(
var tsl: Int = 1693318384,
var upt: Int = 100,
var urc: List<Any> = listOf("INIT", DeviceMessageDownlink()),
var cID: Int = 49093243
) {
fun setURC(returnCodes: List<String>, downlink: String) {
this.urc = returnCodes + DeviceMessageDownlink(downlink)
}
}
@JsonProperty("cID") var cid: Int = 49093243
)

@JsonNaming(PropertyNamingStrategies.UpperSnakeCaseStrategy::class)
data class DeviceMessageDownlink(var dl: String = "0")
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,6 @@ package org.gxf.crestdevicesimulator.simulator.message

import com.fasterxml.jackson.databind.JsonNode
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.node.ArrayNode
import com.fasterxml.jackson.databind.node.IntNode
import com.fasterxml.jackson.databind.node.JsonNodeFactory
import com.fasterxml.jackson.databind.node.ObjectNode
import com.fasterxml.jackson.databind.node.TextNode
import io.github.oshai.kotlinlogging.KotlinLogging
import org.eclipse.californium.core.CoapClient
import org.eclipse.californium.core.CoapResponse
Expand All @@ -32,39 +27,51 @@ class MessageHandler(
private val pskService: PskService,
private val mapper: ObjectMapper,
private val commandService: CommandService,
private val handlers: List<CommandHandler>
private val handlers: MutableList<out CommandHandler>,
private val jacksonObjectMapper: ObjectMapper
) {
private val logger = KotlinLogging.logger {}

companion object {
private const val URC_FIELD = "URC"
private const val URC_PSK_SUCCESS = "PSK:SET"
private const val URC_PSK_ERROR = "PSK:EQER"
private const val REBOOT_SUCCESS = "INIT"
private const val DL_FIELD = "DL"
}

fun sendMessage(jsonNode: JsonNode, simulatorState: SimulatorState) {
val newMessage = jsonNode as ObjectNode
newMessage.replace("FMC", IntNode(simulatorState.fotaMessageCounter))
logger.info { "Sending request: $newMessage" }
val request = createRequest(newMessage)
simulatorState.resetUrcs()
fun sendMessage(simulatorState: SimulatorState): Boolean {
val messageToBeSent = createMessageFromCurrentState(simulatorState)
val request = createRequest(messageToBeSent)
simulatorState.resetUrc()

var coapClient: CoapClient? = null
var immediateResponseRequested = false

try {
coapClient = coapClientService.createCoapClient()
val response = coapClient.advanced(request)
logger.info { "Received Response: ${response.payload.decodeToString()} with status ${response.code}" }
handleResponse(response, simulatorState)
immediateResponseRequested = String(response.payload).startsWith("!")
} catch (e: Exception) {
e.printStackTrace()
} finally {
if (coapClient != null) coapClientService.shutdownCoapClient(coapClient)
}

return immediateResponseRequested
}

fun createRequest(jsonNode: JsonNode): Request {
private fun createMessageFromCurrentState(simulatorState: SimulatorState) =
DeviceMessage().apply {
fmc = simulatorState.fotaMessageCounter
urc = simulatorState.getUrcListForDeviceMessage()
}

fun createRequest(message: DeviceMessage): Request {
val jsonNode: JsonNode = jacksonObjectMapper.valueToTree(message)
logger.info { "Sending request: $jsonNode" }
val payload =
if (simulatorProperties.produceValidCbor) CborFactory.createValidCbor(jsonNode)
else CborFactory.createInvalidCbor()
Expand All @@ -91,12 +98,6 @@ class MessageHandler(
}
}
handlers.forEach { handler -> handler.handleResponse(response, simulatorState) }
if (payload != 0.toString()) {
val newMessage = DeviceMessage(fmc = simulatorState.fotaMessageCounter)
newMessage.urc = simulatorState.getUrcListForDeviceMessage()
val jsonNode = mapper.valueToTree<JsonNode>(newMessage)
sendMessage(jsonNode, simulatorState)
}
} else {
logger.error { "Received error response with ${response.code}" }
if (pskService.isPendingKeyPresent()) {
Expand All @@ -120,31 +121,22 @@ class MessageHandler(

private fun sendPskSetSuccessMessage(pskCommand: String, simulatorState: SimulatorState) {
logger.info { "Sending success message for command $pskCommand" }
val messageJsonNode = mapper.readTree(simulatorProperties.successMessage.inputStream)
val message = updatePskCommandInMessage(messageJsonNode, URC_PSK_SUCCESS, pskCommand)
sendMessage(message, simulatorState)
simulatorState.addUrc(URC_PSK_SUCCESS)
simulatorState.addDownlink("PSK:####:SET")
sendMessage(simulatorState)
}

private fun sendPskSetFailureMessage(pskCommand: String, simulatorState: SimulatorState) {
logger.warn { "Sending failure message for command $pskCommand" }
val messageJsonNode = mapper.readTree(simulatorProperties.failureMessage.inputStream)
val message = updatePskCommandInMessage(messageJsonNode, URC_PSK_ERROR, pskCommand)
sendMessage(message, simulatorState)
simulatorState.addUrc(URC_PSK_ERROR)
simulatorState.addDownlink("PSK:####:SET")
sendMessage(simulatorState)
}

private fun sendRebootSuccesMessage(command: String, simulatorState: SimulatorState) {
logger.info { "Sending success message for command $command" }
val message = mapper.readTree(simulatorProperties.rebootSuccessMessage.inputStream)
sendMessage(message, simulatorState)
}

private fun updatePskCommandInMessage(message: JsonNode, urc: String, receivedCommand: String): JsonNode {
val newMessage = message as ObjectNode
val urcList =
listOf(TextNode(urc), ObjectNode(JsonNodeFactory.instance, mapOf(DL_FIELD to TextNode(receivedCommand))))
val urcArray = mapper.valueToTree<ArrayNode>(urcList)
newMessage.replace(URC_FIELD, urcArray)
logger.debug { "Sending message with URC $urcArray" }
return newMessage
simulatorState.addUrc(REBOOT_SUCCESS)
simulatorState.addDownlink("CMD:REBOOT")
sendMessage(simulatorState)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ class FirmwareCommandHandler : CommandHandler {
logger.debug { "Received OTA line $base85Line" }
simulatorState.addDownlink(otaNumberPart)
if (firmwareDone) {
logger.debug { "Firmware done, resetting FMC" }
logger.debug { "Firmware done, resetting FMC and sending OTA:SUC URC" }
simulatorState.fotaMessageCounter = 0
simulatorState.addUrc("OTA:SUC")
} else {
Expand Down
4 changes: 0 additions & 4 deletions application/src/main/resources/application-test-data.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,6 @@
#SPDX-License-Identifier: Apache-2.0
simulator:
config:
scheduled-message: "classpath:messages/kod-alarm-message.json"
success-message: "classpath:messages/psk-change-success-message.json"
failure-message: "classpath:messages/psk-change-failure-message.json"
reboot-success-message: "classpath:messages/kod-reboot-success-message.json"
# Simulator will produce valid/invalid CBOR messages
produce-valid-cbor: true
# pre-shared key for coaps (dtls)
Expand Down

This file was deleted.

This file was deleted.

Loading

0 comments on commit fc83127

Please sign in to comment.