<|start_filename|>src/jvmMain/kotlin/org/bisdk/sdk/Receiver.kt<|end_filename|>
package org.bisdk.sdk

import kotlinx.io.errors.IOException
import org.bisdk.Command
import org.bisdk.Lengths
import org.bisdk.Logger
import org.bisdk.decodeFromGW
import org.bisdk.toHexStringFromGW
import org.bisdk.waitFor
import java.io.DataInputStream

class Receiver(private val dataIn: DataInputStream, private val readTimeout: Int) : Runnable {
    private val queue = mutableListOf<TransportContainer>()
    private var exception: Exception? = null
    private var running = true

    public fun stop() {
        queue.clear()
        running = false
    }

    suspend fun retrieveAnswer(tag: Int): TransportContainer {
        Logger.debug("Waiting for answer with tag $tag")
        waitFor(readTimeout, {
            (queue.find { message -> message.pack.tag == tag } != null) ||
                    (exception != null) ||
                    !running
        }, "PackageReceived")
        throwExceptionIfNoLongerRunning()
        throwExceptionIfOccurred()
        val message = queue.find { message -> message.pack.tag == tag }!!
        queue.remove(message)
        Logger.debug("Answer for tag $tag: $message")
        return message
    }

    private fun throwExceptionIfNoLongerRunning() {
        if (!running) {
            throw InterruptedException("Receiver got signal to stop -> stopping receiving activities ")
        }
    }

    private fun throwExceptionIfOccurred() {
        if (exception != null) {
            val localException = exception
            exception = null
            throw localException!!
        }
    }

    private fun readIncomingPackages() {
        val bytesRead = ArrayList<Byte>()
        var startTime = System.currentTimeMillis()
        Logger.debug("Reading from socket...")
        while (running) {
            if (dataIn.available() > 0) {
                try {
                    while(dataIn.available() > 0) {
                        bytesRead.add(dataIn.readUnsignedByte().toByte())
                    }
                } catch (e: IOException) {
                    exception = e
                }
                if (bytesRead.size >= Lengths.MINIMUM_CONTAINER_SIZE) {  // Only if it contains sender and receiver address
                    Logger.debug("Checking " + bytesRead.toByteArray().toHexStringFromGW())
                    // Perhaps we have some wrong bytes received from the network, we check if we have a valid response without some of the first bytes
                    for (startPos in 0..(bytesRead.size - Lengths.MINIMUM_CONTAINER_SIZE)) {
//                        Logger.debug("startPos: $startPos, bytesReadSize: ${bytesRead.size}")
                        val ba = bytesRead.subList(startPos, bytesRead.size).toByteArray().decodeFromGW()
                        val tc = TransportContainer.from(ba)
                        if (tc.pack.command == Command.EMPTY) {  // TOO much noise
//                            Logger.debug("Empty / unreadable answer received: " + ba.toHexString())
                        }
                        if (tc.hasCorrectChecksum() && tc.pack.command != Command.EMPTY) {
                            if (startPos > 0) {
                                Logger.debug("Ignoring first $startPos bytes: " + bytesRead.subList(0, startPos).toByteArray().toHexStringFromGW())
                            }
                            Logger.debug("Received: $tc in " + (System.currentTimeMillis() - startTime) + "ms")
                            queue.add(tc)
                            if(startPos > 0 || (tc.length() * 2) < (bytesRead.size - startPos)) {
                                Logger.debug("Remaining bytes found startPos: $startPos, tc.size(): ${tc.length() * 2}, bytesRead.size: ${bytesRead.size} => saving...")
                                val remainingBytes = bytesRead.subList(startPos, tc.length() * 2)
                                bytesRead.clear()
                                bytesRead.addAll(remainingBytes)
                            } else {
                                bytesRead.clear()
                            }
                            startTime = System.currentTimeMillis()
                            break
                        }
                    }
                }
            } else {
                Thread.sleep(100)
            }
        }
        Logger.debug("End of reading Thread!")
    }

    override fun run() {
        readIncomingPackages()
    }

}


<|start_filename|>src/jvmMain/kotlin/org/bisdk/sdk/BiPackage.kt<|end_filename|>
package org.bisdk.sdk

import org.bisdk.BiError
import org.bisdk.Command
import org.bisdk.Lengths
import org.bisdk.Payload
import org.bisdk.testBit
import org.bisdk.toByteArray
import org.bisdk.toHexByteArray
import org.bisdk.toHexString


/**
 * A BiPackage is sent over the socket to the gateway. It contains the command and the payload.
 */
data class BiPackage(
    val command: Command,
    val tag: Int = 0,
    val token: String = "0<PASSWORD>",
    val payload: Payload = Payload.empty(),
    val isResponse: Boolean = false,
    val checksum: Byte = 0
) {

    val calculatedChecksum = PackageChecksum(this).calculate().toByte()

    init {
        assert(token.length == 8)
    }

    fun hasCorrectChecksum() = checksum == calculatedChecksum

    fun toByteArray(): ByteArray {
        return getLength().toByteArray(2)
            .plus(tag.toByte().toByteArray())
            .plus(token.toHexByteArray())
            .plus(getCommandCode().toByteArray())
            .plus(payload.toByteArray())
            .plus(calculatedChecksum.toByteArray())
    }

    fun getCommandCode(): Byte {
        val code = command.code
        if (isResponse) {
            val flipped = code or (1 shl 7)
            return flipped.toByte()
        }
        return code.toByte()
    }

    fun getLength(): Int {
        return getFrameLength() + payload.toByteArray().size
    }

    private fun getFrameLength(): Int {
        return Lengths.FRAME_SIZE / Lengths.BYTE_LENGTH
    }

    fun toHexString(): String {
        return toByteArray().toHexString()
    }

    fun getBiError(): BiError? = BiError.from(payload.toByteArray()[0].toInt())

    override fun toString(): String {
        if (command == Command.ERROR) {
            return "command: $command, tag: $tag, token: $token, error: ${getBiError()}, isResponse=$isResponse"
        }
        if (command == Command.HM_GET_TRANSITION && isResponse) {
            return "command: $command, tag: $tag, token: $token, payload: ${Transition.from(payload.toByteArray())}, isResponse=$isResponse"
        }
        return "command: $command, tag: $tag, token: $token, payload: $payload, isResponse=$isResponse, checksum=$checksum"
    }

    companion object {
        fun empty() = BiPackage(Command.EMPTY)
        fun login(username: String, password: String) =
            fromCommandAndPayload(command = Command.LOGIN, payload = Payload.login(username, password))

        fun jmcp(content: String) = fromCommandAndPayload(command = Command.JMCP, payload = Payload.jmcp(content))

        fun fromCommandAndPayload(command: Command, payload: Payload): BiPackage {
            val packWithoutChecksum = BiPackage(command = command, payload = payload)
            val checksum = PackageChecksum(packWithoutChecksum).calculate().toByte()
            return BiPackage(command = command, payload = payload, checksum = checksum)
        }

        fun from(ba: ByteArray): BiPackage {
            if (ba.size < Lengths.LENGTH_BYTES + Lengths.TAG_BYTES + Lengths.TOKEN_BYTES + Lengths.COMMAND_BYTES) {
                return empty()
            }
            var idx = 0
            val lengthHexString = ba.copyOfRange(idx, idx + Lengths.LENGTH_BYTES).toHexString()
            val length = lengthHexString.toInt(16)
            idx += Lengths.LENGTH_BYTES
            val tagHexString = ba.copyOfRange(idx, idx + Lengths.TAG_BYTES).toHexString()
            val tag = tagHexString.toInt(16)
            idx += Lengths.TAG_BYTES
            val token = ba.copyOfRange(idx, idx + Lengths.TOKEN_BYTES).toHexString()
            idx += Lengths.TOKEN_BYTES
            val commandHexString = ba.copyOfRange(idx, idx + Lengths.COMMAND_BYTES).toHexString()
            var commandInt = commandHexString.toInt(16)
            var isResponse = false
            if (commandInt.testBit(7)) {
                commandInt = commandInt xor (1 shl 7)
                isResponse = true
            }
            idx += Lengths.COMMAND_BYTES
            val command = Command.valueOf(commandInt)
//            Logger.debug("length: $lengthHexString, tag: $tagHexString, token: $token, command: $commandHexString ($command)")
            val payloadLength =
                length - Lengths.LENGTH_BYTES - Lengths.TAG_BYTES - Lengths.TOKEN_BYTES - Lengths.COMMAND_BYTES - Lengths.CHECKSUM_BYTES
            if (payloadLength < 0 || ba.size < length) {
                return empty()
            }
//            Logger.debug("ba.size: ${ba.size}, idx: $idx, Payload length: $payloadLength")
            val payloadBytes = ba.copyOfRange(idx, idx + payloadLength)
            val checksum = ba[idx + payloadLength]
//            Logger.debug("Payload: ${payloadBytes.toHexString()} Checksum: ${checksum.toHexString()}")
            val biPackage = BiPackage(command, tag, token, Payload(payloadBytes), isResponse, checksum)
//            Logger.debug("Parsed BiPackage: ${biPackage.toHexString()}")
            if (!biPackage.hasCorrectChecksum()) {
//                Logger.debug("Incorrect checksum in package: parsed: $checksum, calculated: ${biPackage.calculatedChecksum}")
                return empty()
            }
            return biPackage
        }

        fun fromHexString(hex: String): BiPackage {
            return from(hex.toHexByteArray())
        }
    }

}



<|start_filename|>src/commonMain/kotlin/org/bisdk/Logger.kt<|end_filename|>
package org.bisdk

import kotlinx.datetime.Clock


/**
 * Abstraction of a basic logger to be used by external applications.
 *
 * Set your own LoggerAdapter and include BiSdk in your own logging framework.
 */
class Logger {

    companion object {
        private var internalAdapter: LoggerAdapter = PrintlnAdapter()
        fun setLoggerAdapter(newLoggerAdapter: LoggerAdapter) {
            internalAdapter = newLoggerAdapter
        }

        fun debug(message: String) {
            internalAdapter.debug(message)
        }

        fun info(message: String) {
            internalAdapter.info(message)
        }

        fun warn(message: String) {
            internalAdapter.warn(message)
        }
    }
}

class PrintlnAdapter : LoggerAdapter {
    override fun debug(message: String) {
        println(Clock.System.now().toString() + " DEBUG: " + message)
    }

    override fun info(message: String) {
        println(Clock.System.now().toString() + " INFO: " + message)
    }

    override fun warn(message: String) {
        println(Clock.System.now().toString() + " WARN: " + message)
    }
}

class NoopAdapter : LoggerAdapter {
    override fun debug(message: String) {
    }

    override fun info(message: String) {
    }

    override fun warn(message: String) {
    }
}

interface LoggerAdapter {
    fun debug(message: String)
    fun info(message: String)
    fun warn(message: String)
}


<|start_filename|>src/commonMain/kotlin/org/bisdk/JsonTypes.kt<|end_filename|>
package org.bisdk.sdk

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
data class Group(
    @SerialName("ID")
    val id: Int,
    @SerialName("NAME")
    val name: String,
    @SerialName("PORTS")
    val ports: List<Port>
)

@Serializable
data class Port(
    @SerialName("ID")
    val id: Int,
    @SerialName("TYPE")
    val type: Int
) {
    override fun toString(): String {
        return "Port(id=$id, type=$type (${PortType.from(type)}))"
    }
}

enum class PortType(val code: Int) {
    NONE(0),
    IMPULS(1),
    AUTO_CLOSE(2),
    ON_OFF(3),
    UP(4),
    DOWN(5),
    HALF(6),
    WALK(7),
    LIGHT(8),
    ON(9),
    OFF(10),
    LOCK(11),
    UNLOCK(12),
    OPEN_DOOR(13),
    LIFT(14),
    SINK(15);

    companion object {
        fun from(code: Int): PortType? = values().firstOrNull { it.code == code }
    }
}

enum class GroupType(val code: Int, val ports: List<PortType> = emptyList()) {
    NONE(0),
    SECTIONAL_DOOR(1, listOf(PortType.IMPULS, PortType.UP, PortType.DOWN, PortType.HALF, PortType.LIGHT)),
    HORIZONTAL_SECTIONAL_DOOR(2, listOf(PortType.IMPULS, PortType.UP, PortType.DOWN, PortType.HALF, PortType.LIGHT)),
    SWING_GATE_SINGLE(3, listOf(PortType.IMPULS, PortType.HALF, PortType.UP, PortType.DOWN, PortType.LIGHT)),
    SWING_GATE_DOUBLE(4, listOf(PortType.IMPULS, PortType.WALK, PortType.UP, PortType.DOWN, PortType.LIGHT)),
    SLIDING_GATE(5, listOf(PortType.IMPULS, PortType.UP, PortType.DOWN, PortType.HALF, PortType.LIGHT)),
    DOOR(6, listOf(PortType.AUTO_CLOSE, PortType.IMPULS, PortType.LIGHT, PortType.ON_OFF)),
    LIGHT(7, listOf(PortType.ON, PortType.OFF, PortType.ON_OFF, PortType.IMPULS)),
    OTHER(8, listOf(PortType.ON, PortType.OFF, PortType.ON_OFF, PortType.IMPULS)),
    JACK(9, listOf(PortType.ON, PortType.OFF, PortType.ON_OFF, PortType.IMPULS)),
    SMARTKEY(10, listOf(PortType.LOCK, PortType.UNLOCK, PortType.OPEN_DOOR)),
    PILOMAT_POLLER(11, listOf(PortType.LIFT, PortType.SINK)),
    PILOMAT_DURCHFAHRTSSPERRE(12, listOf(PortType.LIFT, PortType.SINK)),
    PILOMAT_HUBBALKEN(13, listOf(PortType.LIFT, PortType.SINK)),
    PILOMAT_REIFENKILLER(14, listOf(PortType.LIFT, PortType.SINK)),
    BARRIER(15, listOf(PortType.IMPULS, PortType.UP, PortType.DOWN));

    companion object {
        fun from(code: Int): GroupType? = values().firstOrNull { it.code == code }
    }
}


<|start_filename|>src/jvmMain/kotlin/org/bisdk/sdk/ClientAPI.kt<|end_filename|>
package org.bisdk.sdk


import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import org.bisdk.AuthenticationException
import org.bisdk.BiError
import org.bisdk.Command
import org.bisdk.Logger
import org.bisdk.Payload
import java.net.SocketException

/**
 * The ClientAPI is the high-level API for sending commands to the BiSecure Gateway.
 * It is the preferred way to use the BiSDK library.
 *
 * A ClientAPI needs a GatewayConnection to work. This which can be retrieved / initialized with the result of the discovery.
 *
 * Example usage:
 *
 *      val discovery = Discovery()
 *      val future = discovery.startServer()
 *      discovery.sendDiscoveryRequest()
 *      val discoveryData = future.join()
 *      val client = GatewayConnection(address = discoveryData.sourceAddress, gatewayId =  discoveryData.getGatewayId())
 *      val clientAPI = ClientAPI(client)
 *
 * Most of the commands need you to authorize first:
 *      clientAPI.login("username", "password")
 *  This will create a session and the token will be automatically used for the next commands.
 *  After you are done, you can logout again:
 *      clientAPI.logout()
 */
class ClientAPI(
    val gatewayConnection: GatewayConnection
) : AutoCloseable {
    private var userName: String? = null
    private var password: String? = null
    private var tag = 0

    suspend fun getName(): String {
        val answer = sendWithRetry(BiPackage.fromCommandAndPayload(Command.GET_NAME, Payload.empty()))
        return answer.payload.getContentAsString()
    }

    suspend fun ping(): Boolean {
        return try {
            val answer = sendDirectly(BiPackage.fromCommandAndPayload(Command.PING, Payload.empty()))
            answer.command == Command.PING
        } catch (e: Exception) {
            false
        }
    }

    suspend fun login(userName: String, password: String): Boolean {
        var i = 3
        while (i-- > 0) {
            val answer = sendWithRetry(
                BiPackage.fromCommandAndPayload(
                    command = Command.LOGIN,
                    payload = Payload.login(userName, password)
                )
            )
            if (answer.command == Command.LOGIN) {
                this.userName = userName
                this.password = password
                return true
            } else {
                Logger.info("Received " + answer.command + " and not LOGIN as expected => retrying...")
                Thread.sleep(500)
            }
        }
        return false
    }

    suspend fun relogin() {
        if (userName == null || password == null) {
            return
        }
        val newTag = getNewTag()
        gatewayConnection.sendMessage(
            BiPackage.fromCommandAndPayload(
                command = Command.LOGIN,
                payload = Payload.login(userName!!, password!!)
            ).copy(tag = newTag)
        )
        gatewayConnection.readAnswer(newTag)
    }

    suspend fun getToken(userName: String, password: String): String {
        if (login(userName, password)) {
            return gatewayConnection.token
        }
        throw AuthenticationException("Could not login")
    }

    suspend fun logout() {
        // We do not send logout with retry since it could be called when something is disposed after some time
        // Chances are very high that the connection is already gone
        gatewayConnection.sendMessage(
            BiPackage.fromCommandAndPayload(
                command = Command.LOGOUT,
                payload = Payload.empty()
            ).copy(tag = getNewTag())
        )
    }

    /**
     * The getState command returns a map of port and some kind of number. For now I don't know how to handle that
     */
    suspend fun getState(): HashMap<String, Int> {
        var i = 3
        while (i-- > 0) {
            val answer = sendWithRetry(
                BiPackage.fromCommandAndPayload(
                    command = Command.JMCP,
                    payload = Payload.getValues()
                )
            )
            val json = answer.payload.getContentAsString()

            try {
                return Json.decodeFromString(json)
            } catch (e: Exception) {
                Logger.info("Could not deserialize Groups from $json, error: " + e.message + " -> retrying...")
                Thread.sleep(500)
            }
        }
        throw IllegalStateException("Retry failed, got error answer! ")
    }

    /**
     * The groups are the paired devices. This call returns all devices known to the GW
     */
    suspend fun getGroups(): List<Group> {
        val answer = sendWithRetry(
            BiPackage.fromCommandAndPayload(
                command = Command.JMCP,
                payload = Payload.getGroups()
            )
        )
        val json = answer.payload.getContentAsString()

        try {
            return Json.decodeFromString(json)
        } catch (e: Exception) {
            Logger.info("Could not deserialize Groups from $json, error: " + e.message + " -> retrying...")
            throw e
        }
    }

    /**
     * This will return only the devices that are paired with the current user. We probably should always use this
     */
    suspend fun getGroupsForUser(): List<Group> {
        val answer = sendWithRetry(
            BiPackage.fromCommandAndPayload(
                command = Command.JMCP,
                payload = Payload.getGroupsForUser()
            )
        )
        val json = answer.payload.getContentAsString()

        try {
            return Json.decodeFromString(json)
        } catch (e: Exception) {
            Logger.info("Could not deserialize Groups from $json, error: " + e.message + " -> retrying...")
            throw e
        }
    }

    /**
     * Triggers an action on the device. For the garage door this means open/close the door (like pressing a button on the hand held)
     */
    suspend fun setState(port: Port): BiPackage {
        return sendWithRetry(
            BiPackage.fromCommandAndPayload(
                command = Command.SET_STATE,
                payload = Payload.setState(port.id)
            )
        )
    }

    /**
     * Returns the current state of the port. You can see how much open it is or if it is still running.
     */
    suspend fun getTransition(port: Port): Transition {
        val answer: BiPackage = sendWithRetry(
            BiPackage.fromCommandAndPayload(
                command = Command.HM_GET_TRANSITION,
                payload = Payload.getTransition(port.id)
            )
        )
        return Transition.from(answer.payload.toByteArray())
    }

    /**
     * Central method to handle all error conditions wile sending and receiving data from the gateway.
     *
     * It will retry certain times and try to handle special error conditions with a good retry / reconnect / relogin strategy
     */
    private suspend fun sendWithRetry(messageToSend: BiPackage): BiPackage {
        var message = messageToSend.copy(tag = getNewTag())
        var sendCounter = 3L
        while (sendCounter-- > 0) {
            try {
                gatewayConnection.sendMessage(message)
                break
            } catch (e: Exception) {
                Logger.info("Received Exception ${e.message} => reconnecting...")
                gatewayConnection.reconnect()
                relogin()
                Thread.sleep((4 - sendCounter) * 100) // Increase waiting time for each retry
            }
        }
        var error = "N/A"
        var i = 2L
        while (i-- > 0) {
            try {
                val tc = gatewayConnection.readAnswer(message.tag)
                val command = tc.pack.command
                if (tc.pack.command == Command.ERROR && tc.pack.getBiError() != null && tc.pack.getBiError() == BiError.PERMISSION_DENIED) {
                    Logger.debug("Received PERMISSION_DENIED => relogin...")
                    relogin()
                    message =
                        messageToSend.copy(tag = getNewTag())  // try with new tag to see in log file which message was answered
                    gatewayConnection.sendMessage(message)
                    Thread.sleep(500)
                } else if (command == Command.ERROR) {
                    Logger.debug("Received ERROR (" + tc.pack.getBiError() + ") answer => retrying...")
                    error = "Received ERROR answer"
                    message =
                        messageToSend.copy(tag = getNewTag())  // try with new tag to see in log file which message was answered
                    gatewayConnection.sendMessage(message)
                    Thread.sleep(500)
                } else if (command == Command.EMPTY && i == 1L) {
                    Logger.info("Received EMPTY answer 2 times => reconnecting...")
                    error = "Received EMPTY answer"
                    gatewayConnection.reconnect()
                    relogin()
                    message =
                        messageToSend.copy(tag = getNewTag())  // try with new tag to see in log file which message was answered
                    gatewayConnection.sendMessage(message)
                    Thread.sleep(500)
                } else if (command == Command.EMPTY) {
                    Logger.debug("Received EMPTY answer => waiting...")
                    error = "Received EMPTY answer"
                    Thread.sleep(500)
                } else {
                    return tc.pack
                }
            } catch (e: SocketException) {
                Logger.info("Received SocketException ${e.message} => reconnecting...")
                gatewayConnection.reconnect()
                relogin()
                message =
                    messageToSend.copy(tag = getNewTag())  // try with new tag to see in log file which message was answered
                gatewayConnection.sendMessage(message)
                Thread.sleep(500)
            } catch (e: IllegalStateException) {
                Logger.debug(
                    "Received Exception ${e.message} (which probably comes from a timeout while reading). " +
                            "This happens after a while (about 2min), the GW just stops responding => retrying..."
                )
                error = "Received Exception ${e.message}"
                gatewayConnection.reconnect()
                relogin()
                message =
                    messageToSend.copy(tag = getNewTag())  // try with new tag to see in log file which message was answered
                gatewayConnection.sendMessage(message)
            } catch (e: Exception) {
                Logger.info("Received Exception ${e.message} => retrying...")
                error = "Received Exception ${e.message}"
                Thread.sleep(500)
            }

        }
        throw IllegalStateException("Retry failed: $error")
    }

    /**
     * Send without retry and error handling
     */
    private suspend fun sendDirectly(messageToSend: BiPackage): BiPackage {
        val message = messageToSend.copy(tag = getNewTag())
        gatewayConnection.sendMessage(message)
        val tc = gatewayConnection.readAnswer(message.tag)
        return tc.pack
    }

    private fun getNewTag(): Int {
        if (tag >= 128) {
            tag = 0
        } else {
            tag++
        }
        return tag
    }

    override fun close() {
        gatewayConnection.close()
    }

}


<|start_filename|>src/commonMain/kotlin/org/bisdk/Utils.kt<|end_filename|>
package org.bisdk

import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.withContext

/**
 * Waits for the given condition to become true. Checks every 50ms if the condition is true.
 * If senderTimeout has exceeded the errorAction is executed (if exists) and an IllegalStateException is thrown
 */
suspend fun waitFor(timeout: Int, condition: () -> Boolean, name: String = "", errorAction: (() -> Any)? = null) =
    withContext(
        Dispatchers.Default
    ) {
        var waited = 0
        while (!condition()) {
            if (waited > timeout) {
                if (errorAction != null) {
                    errorAction()
                }
                throw IllegalStateException("Timeout waiting for $name (after waiting $timeout ms)")
            }
            waited += 50
            delay(50)
        }
    }


<|start_filename|>src/jvmTest/kotlin/org/bisdk/sdk/RealGatewayTest.kt<|end_filename|>
package org.bisdk.sdk

import kotlinx.coroutines.runBlocking
import org.bisdk.Logger
import org.junit.Before
import org.junit.Test

internal class RealGatewayTest {

    // TODO: migrate to unit5 and have conditional test
    // @EnabledIfEnvironmentVariable(named = "bisecureUsername", matches = ".+")
    private var ignoreTests: Boolean = false
    private lateinit var username: String
    private lateinit var password: String

    @Before
    fun setup() {
        username = System.getenv("bisecureUsername") ?: ""
        password = System.getenv("<PASSWORD>curePassword") ?: ""
        ignoreTests = username.isEmpty()
    }

    /**
     * For this test to work you will need a real GW in your network
     * with the username and password set in your environment variables
     *
     * The startup flow:
     * <ul>
     *     <li>Discovery of gateway</li>
     *     <li>Get Name</li>
     *     <li>Login</li>
     *     <li>Logout</li>
     *  </ul>
     */
    @Test
    fun testStartup()  = runBlocking {
        if (ignoreTests) {
            return@runBlocking
        }

        val discovery = Discovery()
        val future = discovery.startServer()
        discovery.sendDiscoveryRequest()
        val discoveryData = future.join()

        val client = GatewayConnection(discoveryData.sourceAddress, "000000000000", discoveryData.getGatewayId())
        val clientAPI = ClientAPI(client)
        println("Name: " + clientAPI.getName())
        println("Ping: " + clientAPI.ping())
        println("Login in...")
        clientAPI.login(username, password)
        val state = clientAPI.getState()
        println("State: $state")
        val groups = clientAPI.getGroups()
        println("Groups: $groups")
        val transition = clientAPI.getTransition(groups[0].ports[0])
//        clientAPI.setState(groups[0].ports[0])
        clientAPI.logout()

    }

    @Test
    fun testPing()  = runBlocking {
        if (ignoreTests) {
            return@runBlocking
        }
        val discovery = Discovery()
        val future = discovery.startServer()
        discovery.sendDiscoveryRequest()
        val discoveryData = future.join()

        val client = GatewayConnection(discoveryData.sourceAddress, "000000000000", discoveryData.getGatewayId())
        val clientAPI = ClientAPI(client)
        var i: Int = 0
        val startTime = System.currentTimeMillis()
        while (i++ < 1000 && clientAPI.ping()) {
            Thread.sleep(1000)
        }
        val timeElapsed = System.currentTimeMillis() - startTime
        Logger.info("Ping succeded for " + timeElapsed + "ms")
    }

    @Test
    fun testGetTransitionTiming() = runBlocking {
        if (ignoreTests) {
            return@runBlocking
        }
        val discovery = Discovery()
        val future = discovery.startServer()
        discovery.sendDiscoveryRequest()
        val discoveryData = future.join()
        val client = GatewayConnection(address = discoveryData.sourceAddress, gatewayId = discoveryData.getGatewayId())
        val clientAPI = ClientAPI(client)
        clientAPI.login("username", "password")
        val groups = clientAPI.getGroups()
        var i = 0
        val times = mutableListOf<Long>()
        var errors = 0
        val startTime = System.currentTimeMillis()
        while (i++ < 200) {
            val localStartTime = System.currentTimeMillis()
            try {
                clientAPI.getTransition(groups[0].ports[0])
            } catch (e: Exception) {
                errors++
            }
            val localTimeElapsed = System.currentTimeMillis() - localStartTime
            times.add(localTimeElapsed)
        }
        val timeElapsed = System.currentTimeMillis() - startTime
        Logger.info("Total time " + timeElapsed + "ms")
        Logger.info("Times: " + times)
        val under1sec = times.filter { it < 1000 }.count()
        val under2sec = times.filter { it < 2000 }.count()
        val under5sec = times.filter { it < 5000 }.count()
        Logger.info("Under 1s: $under1sec, Under 2s: $under2sec, Under 5s: $under5sec, errors: $errors ")
    }

    @Test
    fun testGetGroupsTiming() = runBlocking {
        if (ignoreTests) {
            return@runBlocking
        }
        val discovery = Discovery()
        val future = discovery.startServer()
        discovery.sendDiscoveryRequest()
        val discoveryData = future.join()
        val client = GatewayConnection(address = discoveryData.sourceAddress, gatewayId = discoveryData.getGatewayId())
        val clientAPI = ClientAPI(client)
        clientAPI.login("username", "password")
        var i = 0
        val times = mutableListOf<Long>()
        var errors = 0
        val startTime = System.currentTimeMillis()
        while (i++ < 200) {
            val localStartTime = System.currentTimeMillis()
            try {
                clientAPI.getGroups()
            } catch (e: Exception) {
                errors++
            }
            val localTimeElapsed = System.currentTimeMillis() - localStartTime
            times.add(localTimeElapsed)
        }
        val timeElapsed = System.currentTimeMillis() - startTime
        Logger.info("Total time " + timeElapsed + "ms")
        Logger.info("Times: " + times)
        val under1sec = times.filter { it < 1000 }.count()
        val under2sec = times.filter { it < 2000 }.count()
        val under5sec = times.filter { it < 5000 }.count()
        Logger.info("Under 1s: $under1sec, Under 2s: $under2sec, Under 5s: $under5sec, errors: $errors ")
    }

}


<|start_filename|>src/jvmMain/kotlin/org/bisdk/sdk/Sender.kt<|end_filename|>
package org.bisdk.sdk

import org.bisdk.Logger
import org.bisdk.encodeToGW
import org.bisdk.waitFor
import java.io.DataOutputStream

/**
 * A Runnable that sends packets whenever they arrive.
 *
 * Used for non - blocking sending, especially when network errors block sending operations
 */
class Sender(private val dataOut: DataOutputStream, private val sendTimeout: Int) : Runnable {
    private val queue = mutableListOf<TransportContainer>()
    private var running = true
    private var exception: Exception? = null

    public suspend fun send(message: TransportContainer) {
        waitFor(sendTimeout, {queue.isEmpty()}, "EmptyQueue")
        queue.add(message)
        waitFor(sendTimeout, {!queue.contains(message)}, "MessageProcessed", {queue.remove(message)})
        throwExceptionIfOccurred()
    }

    public fun stop() {
        running = false
    }

    private fun throwExceptionIfOccurred() {
        if (exception != null) {
            val localException = exception
            exception = null
            throw localException!!
        }
    }

    override fun run() {
        while (running) {
            while (running && queue.isEmpty()) {
                Thread.sleep(50)
            }
            // If we were stopped in the meantime, return here
            if(!running) {
                return
            }
            val message = queue[0]
            val messageBytes = message.toByteArray().encodeToGW()
            Logger.debug("Sending transport container ${message.toHexString()}")
            try {
                dataOut.write(messageBytes)
                dataOut.flush()
            } catch (e: Exception) {
                exception = e
            }
            queue.remove(message)
            Logger.debug("Sending done")
        }
    }
}
