All Downloads are FREE. Search and download functionalities are using the official Maven repository.

p-sim-ecu.doip-sim-ecu-dsl.0.15.1.source-code.SimDsl.kt Maven / Gradle / Ivy

Go to download

This is a kotlin based domain specific language (dsl), to quickly and intuitively write custom DoIP ECU simulations.

The newest version!
@file:Suppress("MemberVisibilityCanBePrivate")

import library.*
import java.util.*
import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds

public typealias RequestResponseData = ResponseData
public typealias RequestResponseHandler = RequestResponseData.() -> Unit
public typealias InterceptorRequestData = ResponseData
public typealias InterceptorRequestHandler = InterceptorRequestData.(request: RequestMessage) -> Boolean
public typealias InterceptorResponseHandler = InterceptorResponseData.(response: ByteArray) -> Boolean
public typealias EcuDataHandler = EcuData.() -> Unit
public typealias DoipEntityDataHandler = DoipEntityData.() -> Unit
public typealias NetworkingDataHandler = NetworkingData.() -> Unit
public typealias CreateEcuFunc = (name: String, receiver: EcuDataHandler) -> Unit
public typealias CreateDoipEntityFunc = (name: String, receiver: DoipEntityDataHandler) -> Unit
public typealias CreateNetworkFunc = (receiver: NetworkingDataHandler) -> Unit

@Suppress("unused")
public class InterceptorResponseData(
    caller: ResponseInterceptorData,
    request: UdsMessage,
    public val responseMessage: ByteArray,
    ecu: SimEcu
) : ResponseData(caller, request, ecu)

public open class NrcException(public val code: Byte) : Exception()

@Suppress("unused", "ConstPropertyName")
public object NrcError {
    // Common Response Codes
    public const val GeneralReject: Byte = 0x10
    public const val ServiceNotSupported: Byte = 0x11
    public const val SubFunctionNotSupported: Byte = 0x12
    public const val IncorrectMessageLengthOrInvalidFormat: Byte = 0x13
    public const val ResponseTooLong: Byte = 0x14

    public const val BusyRepeatRequest: Byte = 0x21
    public const val ConditionsNotCorrect: Byte = 0x22

    public const val RequestSequenceError: Byte = 0x24
    public const val NoResponseFromSubNetComponent: Byte = 0x25
    public const val FailurePreventsExecutionOfRequestedAction: Byte = 0x26

    public const val RequestOutOfRange: Byte = 0x31

    public const val SecurityAccessDenied: Byte = 0x33
    public const val AuthenticationRequired: Byte = 0x34

    public const val InvalidKey: Byte = 0x35
    public const val ExceededNumberOfAttempts: Byte = 0x36
    public const val RequiredTimeDelayNotExpired: Byte = 0x37
    public const val SecureDataTransmissionRequired: Byte = 0x38
    public const val SecureDataTransmissionNotAllowed: Byte = 0x39
    public const val SecureDataVerificationFailed: Byte = 0x3a

    public const val CertificateVerificationFailedInvalidTimePeriod: Byte = 0x50
    public const val CertificateVerificationFailedInvalidSignature: Byte = 0x51
    public const val CertificateVerificationFailedInvalidChainOfTrust: Byte = 0x52
    public const val CertificateVerificationFailedInvalidType: Byte = 0x53
    public const val CertificateVerificationFailedInvalidFormat: Byte = 0x54
    public const val CertificateVerificationFailedInvalidContent: Byte = 0x55
    public const val CertificateVerificationFailedInvalidScope: Byte = 0x56
    public const val CertificateVerificationFailedInvalidCertRevoked: Byte = 0x57
    public const val OwnershipVerificationFailed: Byte = 0x58
    public const val ChallengeCalculationFailed: Byte = 0x59
    public const val SettingAccessRightsFailed: Byte = 0x5a
    public const val SessionKeyCreationDerivationFailed: Byte = 0x5b
    public const val ConfigurationDataUsageFailed: Byte = 0x5c
    public const val DeAuthenticationFailed: Byte = 0x5d

    public const val UploadDownloadNotAccepted: Byte = 0x70
    public const val TransferDataSuspended: Byte = 0x71
    public const val GeneralProgrammingFailure: Byte = 0x72
    public const val WrongBlockSequenceCounter: Byte = 0x73

    public const val RequestCorrectlyReceivedButResponseIsPending: Byte = 0x78

    public const val SubFunctionNotSupportedInActiveSession: Byte = 0x7E
    public const val ServiceNotSupportedInActiveSession: Byte = 0x7F

    public const val RpmTooHigh: Byte = 0x81.toByte()
    public const val RpmTooLow: Byte = 0x82.toByte()
    public const val EngineIsRunning: Byte = 0x84.toByte()
    public const val EngineRunTimeTooLow: Byte = 0x85.toByte()
    public const val TemperatureTooHigh: Byte = 0x86.toByte()
    public const val TemperatureTooLow: Byte = 0x87.toByte()
    public const val VehicleSpeedToHigh: Byte = 0x88.toByte()
    public const val VehicleSpeedTooLow: Byte = 0x89.toByte()
    public const val ThrottleOrPedalTooHigh: Byte = 0x8a.toByte()
    public const val ThrottleOrPedalTooLow: Byte = 0x8b.toByte()
    public const val TransmissionRangeNotInNeutral: Byte = 0x8c.toByte()
    public const val TransmissionRangeNotInGear: Byte = 0x8d.toByte()

    public const val BrakeSwitchesNotClosed: Byte = 0x8f.toByte()

    public const val TorqueConvertedClutchLocked: Byte = 0x91.toByte()
    public const val VoltageTooHigh: Byte = 0x92.toByte()
    public const val VoltageTooLow: Byte = 0x93.toByte()
    public const val ResourceTemporarilyNotAvailable: Byte = 0x94.toByte()
}

public class RequestMessage(udsMessage: UdsMessage, public val isBusy: Boolean) :
    UdsMessage(
        udsMessage.sourceAddress,
        udsMessage.targetAddress,
        udsMessage.targetAddressType,
        udsMessage.targetAddressPhysical,
        udsMessage.message,
        udsMessage.output
    )

/**
 * Define the response to be sent after the function returns
 */
public open class ResponseData(
    /**
     * The object that called this response handler (e.g. [RequestMatcher] or [InterceptorData])
     */
    public val caller: T,
    /**
     * The request as received for the ecu
     */
    public val request: UdsMessage,
    /**
     * Represents the simulated ecu, allows you to modify data on it
     */
    public val ecu: SimEcu
) {
    /**
     * The request as a byte-array for easier access
     */
    public val message: ByteArray
        get() = request.message

    /**
     * The response that's scheduled to be sent after the response handler
     * finishes
     */
    public val response: ByteArray
        get() = _response

    /**
     * Continue matching with other request handlers
     */
    public val continueMatching: Boolean
        get() = _continueMatching

    public val pendingFor: Duration?
        get() = _pendingFor

    public val pendingForInterval: Duration?
        get() = _pendingForInterval

    public val pendingForNrc: Byte?
        get() = _pendingForNrc

    public val pendingForCallback: () -> Unit
        get() = _pendingForCallback

    public val hardResetEntityFor: Duration?
        get() = _hardResetEntityFor

    private var _response: ByteArray = ByteArray(0)
    private var _continueMatching: Boolean = false
    private var _pendingFor: Duration? = null
    private var _pendingForInterval: Duration? = null
    private var _pendingForNrc: Byte? = null
    private var _pendingForCallback: () -> Unit = {}
    private var _hardResetEntityFor: Duration? = null

    /**
     * See [SimEcu.addOrReplaceTimer]
     */
    public fun addOrReplaceEcuTimer(name: String, delay: Duration, handler: TimerTask.() -> Unit) {
        ecu.addOrReplaceTimer(name, delay, handler)
    }

    /**
     * See [SimEcu.cancelTimer]
     */
    public fun cancelEcuTimer(name: String) {
        ecu.cancelTimer(name)
    }

    /**
     * See [SimEcu.addOrReplaceEcuInterceptor]
     */
    public fun addOrReplaceEcuInterceptor(
        name: String = UUID.randomUUID().toString(),
        duration: Duration = Duration.INFINITE,
        alsoCallWhenEcuIsBusy: Boolean = false,
        interceptor: InterceptorRequestHandler
    ): String =
        ecu.addOrReplaceEcuInterceptor(name, duration, alsoCallWhenEcuIsBusy, interceptor)

    /**
     * See [SimEcu.removeInterceptor]
     */
    public fun removeEcuInterceptor(name: String): RequestInterceptorData? =
        ecu.removeInterceptor(name)

    public fun respond(responseHex: ByteArray) {
        _response = responseHex
    }

    public fun respond(responseHex: String): Unit =
        respond(responseHex.decodeHex())

    /**
     * Acknowledge a request with the given payload. The first nrOfRequestBytes
     * bytes (SID + 0x40, ...) are automatically prefixed.
     *
     * nrOfRequestBytes is the total number of bytes (including SID + 0x40)
     */
    public fun ack(payload: ByteArray = ByteArray(0), nrOfRequestBytes: Int): Unit =
        respond(
            byteArrayOf(
                (message[0] + 0x40.toByte()).toByte(),
                *message.copyOfRange(1, nrOfRequestBytes),
                *payload
            )
        )

    /**
     * Acknowledge a request with the given payload. The first nrOfRequestBytes
     * bytes (SID + 0x40, ...) are automatically prefixed
     *
     * payload must be a hex-string.
     * nrOfRequestBytes is the total number of bytes (including SID + 0x40)
     */
    public fun ack(payload: String, nrOfRequestBytes: Int): Unit =
        ack(payload.decodeHex(), nrOfRequestBytes)

    /**
     * Acknowledge a request with the given payload.
     *
     * The first n bytes are automatically prefixed, depending on which service
     * is responded to (see [RequestsData.ackBytesLengthMap])
     */
    public fun ack(payload: String): Unit =
        ack(payload, ecu.ackBytesLengthMap[message[0]] ?: 2)

    /**
     * Acknowledge a request with the given payload.
     *
     * The first n bytes are automatically prefixed, depending on which service
     * is responded to (see [RequestsData.ackBytesLengthMap])
     */
    public fun ack(payload: ByteArray = ByteArray(0)): Unit =
        ack(payload, ecu.ackBytesLengthMap[message[0]] ?: 2)

    /**
     * Send a negative response code (NRC) in response to the request
     */
    public fun nrc(code: Byte = NrcError.GeneralReject): Unit =
        respond(byteArrayOf(0x7F, message[0], code))

    /**
     * Don't send any responses/acknowledgement, and continue matching
     */
    public fun continueMatching(continueMatching: Boolean = true) {
        _continueMatching = continueMatching
    }

    /**
     * Sends a busy wait (NRC 0x78, received but pending) for duration, before calling
     * callback, and sending the reply
     */
    public fun pendingFor(duration: Duration, callback: () -> Unit = {}) {
        _pendingFor = duration
        _pendingForCallback = callback
    }

    /**
     * Overrides the default interval for sending pending NRC
     */
    public fun pendingForInterval(duration: Duration) {
        _pendingForInterval = duration
    }

    /**
     * Changes the default busy wait NRC (0x78) to something different
     */
    public fun pendingForNrc(nrc: Byte) {
        _pendingForNrc = nrc
    }

    /**
     * Pretend to hard-reset entity by disconnecting the current, nd  not being reachable for duration,
     * and sending a vam after being reachable again
     */
    @ExperimentalDoipDslApi
    public fun hardResetEntityFor(duration: Duration) {
        _hardResetEntityFor = duration
    }
}

/**
 * Defines a matcher for an incoming request and the lambdas to be executed when it matches
 */
public class RequestMatcher(
    public val name: String?,
    public val requestBytes: ByteArray,
    public val onlyStartsWith: Boolean = false,
    public val loglevel: LogLevel = LogLevel.DEBUG,
    public val responseHandler: RequestResponseHandler
) : DataStorage() {
    /**
     * Reset persistent storage for this request
     */
    public fun reset() {
        clearStoredProperties()
    }

    public fun matches(request: ByteArray): Boolean {
        return if (onlyStartsWith) {
            request.startsWith(requestBytes)
        } else {
            request.contentEquals(requestBytes)
        }
    }

    override fun toString(): String {
        val sb = StringBuilder()
        sb.append("{ ")
        if (!name.isNullOrEmpty()) {
            sb.append("$name; ")
        }
        if (onlyStartsWith) {
            sb.append("Bytes: ${requestBytes.toHexString(limit = 10)} []")
        } else {
            sb.append("Bytes: ${requestBytes.toHexString(limit = 10)}")
        }
        sb.append(" }")
        return sb.toString()
    }
}

internal fun List.findByName(name: String) =
    this.firstOrNull { it.name == name }

internal fun MutableList.removeByName(name: String): RequestMatcher? {
    val index = this.indexOfFirst { it.name == name }
    if (index >= 0) {
        return this.removeAt(index)
    }
    return null
}

public data class ResetHandler(val name: String?, val handler: (SimEcu) -> Unit)

public enum class LogLevel {
    ERROR,
    INFO,
    DEBUG,
    TRACE,
}

public enum class DuplicateStrategy {
    ERROR,
    REPLACE,
    APPEND,
}

public val DefaultAckBytesLengthMap: Map = mapOf(
    // default is 2
    0x22.toByte() to 3,
    0x2e.toByte() to 3,

    0x31.toByte() to 4, // routine control

    0x34.toByte() to 1,  // request download
    0x35.toByte() to 1,  // request upload
//    0x36.toByte() to 2, // transfer data
    0x37.toByte() to 1, // transfer exit

    0x14.toByte() to 1, // clear dtc
)

public open class RequestsData(
    public val name: String,
    /**
     * Return a nrc when no request could be matched
     */
    public var nrcOnNoMatch: Boolean = true,
    requests: List = emptyList(),
    resetHandler: List = emptyList(),

    /**
     * Map of Request SID to number of ack response byte count
     */
    public var ackBytesLengthMap: Map = DefaultAckBytesLengthMap,
) {
    /**
     * List of all defined requests in the order they were defined
     */
    public val requests: RequestList = RequestList(requests)

    /**
     * List of defined reset handlers
     */
    public val resetHandler: MutableList = mutableListOf(*resetHandler.toTypedArray())


    /**
     * Define request-matcher & response-handler for a gateway or ecu by using an
     * exact matching byte-array
     */
    public fun request(
        /**
         * Byte-Array to match the request
         */
        request: ByteArray,
        /**
         * Name of the expression to be shown in logs
         */
        name: String? = null,
        /**
         * Defines if request is only for the start or for an exact match
         */
        onlyStartsWith: Boolean = false,
        /**
         * Specifies how a duplicated request matcher should be handled
         */
        duplicateStrategy: DuplicateStrategy = DuplicateStrategy.ERROR,
        /**
         * The loglevel used to log when the request matches and its responses
         */
        loglevel: LogLevel = LogLevel.DEBUG,
        /**
         * Handler that is called when the request is matched
         */
        response: RequestResponseHandler = {}
    ): RequestMatcher {
        val req = RequestMatcher(
            name = name,
            requestBytes = request,
            onlyStartsWith = onlyStartsWith,
            loglevel = loglevel,
            responseHandler = response
        )
        when (duplicateStrategy) {
            DuplicateStrategy.APPEND -> requests.add(req)
            DuplicateStrategy.ERROR -> {
                val duplicate = requests.firstOrNull {
                    it.onlyStartsWith == req.onlyStartsWith && it.requestBytes.contentEquals(req.requestBytes)
                }
                if (duplicate != null) {
                    throw IllegalArgumentException("The request is duplicated, existing request: $duplicate - use different duplicateStrategy, or change requestBytes")
                }
                requests.add(req)
            }

            DuplicateStrategy.REPLACE -> {
                val duplicate = requests.filter {
                    it.onlyStartsWith == req.onlyStartsWith && it.requestBytes.contentEquals(req.requestBytes)
                }
                if (duplicate.isNotEmpty()) {
                    requests.removeAll(duplicate)
                }
                requests.add(req)
            }
        }

        return req
    }

    /**
     * Define request/response pair for a gateway or ecu by using a best guess
     * for the type of string that's used.
     *
     * A static request is only hexadecimal digits and whitespaces, and will be converted
     * into a call to the [request]-Method that takes an ByteArray
     *
     * A dynamic match is detected when the string ends with "[]" or ".*". The hex data only
     * has matched to the start of the incoming request then.
     */
    public fun request(
        /**
         * Hex-String to match the request
         */
        reqHex: String,
        /**
         * Name of the expression to be shown in logs
         */
        name: String? = null,
        /**
         * Specifies how a duplicated request matcher should be handled
         */
        duplicateStrategy: DuplicateStrategy = DuplicateStrategy.ERROR,
        /**
         * The loglevel used to log when the request matches and its responses
         */
        loglevel: LogLevel = LogLevel.DEBUG,
        /**
         * Handler that is called when the request is matched
         */
        response: RequestResponseHandler = {}
    ) {
        val trimmed = reqHex.trimEnd()
        val isOpenEnded = trimmed.endsWith("[]") || trimmed.endsWith(".*") || trimmed.endsWith("..")

        request(
            request = trimmed.decodeHex(),
            name = name,
            onlyStartsWith = isOpenEnded,
            duplicateStrategy = duplicateStrategy,
            loglevel = loglevel,
            response = response
        )
    }

    public fun onReset(name: String? = null, handler: (SimEcu) -> Unit) {
        resetHandler.add(ResetHandler(name, handler))
    }
}

/**
 * Define the data associated with the ecu
 */
public open class EcuData(
    name: String,
    /**
     * The logical address under which the gateway shall be reachable
     */
    public var logicalAddress: Short = 0,
    /**
     * The functional address under which the gateway (and other ecus) shall be reachable
     */
    public var functionalAddress: Short = 0,
    /**
     * Interval between sending pending NRC messages (0x78)
     */
    public var pendingNrcSendInterval: Duration = 2.seconds,
    nrcOnNoMatch: Boolean = true,
    requests: List = emptyList(),
    resetHandler: List = emptyList(),
    ackBytesLengthMap: Map = DefaultAckBytesLengthMap,
) : RequestsData(
    name = name,
    nrcOnNoMatch = nrcOnNoMatch,
    requests = requests,
    resetHandler = resetHandler,
    ackBytesLengthMap = ackBytesLengthMap,
)

internal val networks: MutableList = mutableListOf()
internal val networkInstances: MutableList = mutableListOf()

public fun networks(): List =
    networks.toList()

public fun networkInstances(): List =
    networkInstances.toList()

public fun network(receiver: NetworkingDataHandler) {
    val networkingData = NetworkingData()
    receiver.invoke(networkingData)
    networks.add(networkingData)
}

public fun reset() {
    networkInstances.forEach { it.reset() }
}

@Suppress("unused")
public fun start() {
    networkInstances.addAll(networks.map { SimDoipNetworking(it) })

    val networkManager = networkInstances.map { NetworkManager(it.data, it.doipEntities) }

    networkManager.forEach {
        it.start()
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy