[Resolved] Connect API - Kotlin help

Hello folks,

I am trying to see if I can improve my inflight experience and get my hands dirty building an Android app. I choose Infinite Flight’s connect API to get started.

Honestly, I am using help from this library to rewrite in Kotlin. Thank you @paodekuai for the repo.

Note: The debug only works when I test this using a device and NOT with the emulator (for folks reading this infuture)

Current status:

  1. I have a working version of code that discovers and connect to Infinite Sessions and share the IP of the device.
  2. I am able to fetch the Manifest and parse the data.
  3. I am trying to get state of the aircraft with multiple getState calls, here’s where the problem exists.

Issue

  • My first request to get state data comes through and I can interpret the data, but any subsequent request doesn’t get answered from IF, and is stuck trying to receive data.
  • Added some debug to check if the socket connection is open or there are any unread bytes, did not find any lead to investigate there.

Code

// StateID  and datatype are fetched from manifest list for each command.
 private fun getState(stateId: Int, dataType: Int): Any {
        val request =
            ByteBuffer.allocate(8).order(ByteOrder.LITTLE_ENDIAN).putInt(stateId)
                .put(if (false) 1 else 0).array()
        try {
            conn.outputStream.write(request)
            conn.outputStream.flush()
        } catch (e: IOException) {
            throw IOException("Error sending getState request for id $stateId: ${e.message}", e)
        }

        val returnIdBytes = ConnectClientHelper.receive(conn, 4)
        val returnId = ConnectClientHelper.unpack(returnIdBytes, 1) as Int
        val returnLengthBytes = ConnectClientHelper.receive(conn, 4)
        val returnLength = ByteBuffer.wrap(returnLengthBytes).order(ByteOrder.LITTLE_ENDIAN).int

        val payload = ConnectClientHelper.receive(conn, returnLength)
        val value = ConnectClientHelper.unpack(payload, dataType)
        Log.d(TAG, "payload (${String(payload, Charsets.UTF_8)}): $value")

        return value
    }


// below are the methods from ConnectClientHelper class
fun receive(socket: Socket, length: Int): ByteArray {
        val buffer = ByteArray(length)
        var bytesRead = 0
        while (bytesRead < length) {
            val read = socket.getInputStream().read(buffer, bytesRead, length - bytesRead)
            if (read == -1) {
                throw IOException("End of stream reached before reading all bytes")
            }
            bytesRead += read
        }
        Log.d(TAG, "Received bytes: ${buffer.contentToString()}")
        return buffer
    }

    fun unpack(bytes: ByteArray, dataType: Int): Any {
        val buffer = ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN)
        return when (dataType) {
            0 -> buffer.get() != 0.toByte() // Boolean
            1 -> buffer.int // Int
            2 -> buffer.float // Float
            3 -> buffer.double // Double
            4 -> { // String
                val length = buffer.int
                Log.i(TAG, "Length of string $length")
                // Check if there's enough data left for the string
                if (buffer.remaining() < length) {
                    Log.e(TAG, "Not enough data for string $length: ${buffer.remaining()}")
                    throw Exception("Not enough data for string $length: ${buffer.remaining()}")
                }
                val stringBytes = ByteArray(length.toInt())
                Log.i(TAG, "String Bytes $stringBytes")
                buffer.get(stringBytes)
                String(stringBytes, Charsets.UTF_8)
            }

            5 -> buffer.long // Long

            else -> throw IllegalArgumentException("Unsupported data type: $dataType")
        }
    }

Logs

get state for "aircraft/0/systems/fuel/fuel_remaining"
Identifier - Received bytes: [0, 0, 0, 0]
Packet length - Received bytes: [4, 0, 0, 0]
Payload content - Received bytes: [32, 0, 0, 0]

Question

Coding Aviators expert here: Can you help me find the issue or improvement to the code?
I am unable to get past the second getState call. I am not yet sure if the data I received for the first call is accurate.

Thanks in advance form a Lame kotlin coder.

Does your manifest call work and all others (get,set) are not working?

I does fetch the manifest. I am using the IDs from the manifest for the getState call.

Here is the code that fetches the manifest.

    private fun getManifest() {
        val request = ByteBuffer.allocate(8).order(ByteOrder.LITTLE_ENDIAN).putLong(-1).array()
        try {
            conn.outputStream.write(request)
        } catch (e: IOException) {
            throw IOException("Error sending getManifest request: ${e.message}", e)
        }

        val item =
            ConnectClientHelper.receive(
                conn,
                4
            ) // Not used in the original Python code, but kept for consistency
        val lengthBytes = ConnectClientHelper.receive(conn, 4)
        val length = ByteBuffer.wrap(lengthBytes).order(ByteOrder.LITTLE_ENDIAN).int

        Log.d(TAG, "item: ${item.contentToString()}, length: $length")

        var received = 0
        val responseBytes = ByteArray(length)
        while (received < length) {
            val read = conn.inputStream.read(responseBytes, received, length - received)
            if (read == -1) {
                throw IOException("End of stream reached while reading manifest") // Handle end of stream
            }
            received += read
        }

        val response =
            String(responseBytes.copyOfRange(4, responseBytes.size), Charsets.UTF_8).trim()

        val entries = response.split("\n")
        manifest = mutableMapOf()
        entries.forEach { line ->
            val parts = line.split(",")
            try {
                if (parts.size == 3) { // added check to avoid error
                    val (stateId, dataType, idName) = parts

                    manifest[idName] = mutableMapOf(
                        "id" to stateId.toInt(),
                        "data_type" to if (dataType.toIntOrNull() == null) 4 else dataType.toInt()      // Cast to String if conversion fails
                    )
                } else {
                    Log.w(TAG, "Skipping malformed manifest entry: $line")
                }
            } catch (e: Exception) {
                Log.w(TAG, "Error parsing manifest entry: $line", e)
            }

        }
    }

Have you tried using Big Endian? I can’t remember which is correct to use.

I believe it is Little-Endian

I am reading the buffer according
ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN) in my receive method.

Does anything look fishy with the number of bytes I am sending in the request?

ByteBuffer.allocate(8).order(ByteOrder.LITTLE_ENDIAN).putInt(stateId)

I changed the byte allocation to 7 and see response to some of the request, even a second request, but that’s still not perfect. Also unsure of the results yet.

Can you show us the code that generates these logs? I’m not familiar with android / Kotlin, but it’s not clear to me where these come from.

An unsolicited tip: I’d recommend testing with a boolean value like the landing lights state. It’ll be easy to tell if you have a boolean in the right format, as opposed to the time of fuel remaining, which can vary widely and be an unintuitive value.

Well, logging lines might have been redacted from the lines of code I shared. I was adding those logs in the receive function each time a packet arrived.

I modified the labels to indicate what each of them at packet indicated.

I believe I figured the issue. And my hunch about the byte allocations for requests was incorrect. Per documentation, it requires 4 byte Int and a Bit.

However I have been sending requests of 8 Bytes for all requests, which was the problem. Bit more explanation.

  1. The problem started in getting manifest from IFC. You see in my request for manifest, I was writing 3 additional bytes to the server.
val request = ByteBuffer.allocate(8).order(ByteOrder.LITTLE_ENDIAN).putLong(-1).array()

However, since the first 5 bytes were correct, I received response for manifest.

  1. On each follow up request, IFC server kept appending the request bytes, and did not find any meaningful command and I guess responded with all 0’s.
  • Implying, first 2 bytes of my getState request was appended to the 3 extra bytes from the manifest request.
  • Remaining 6 bytes from getState requests were appended to the following getState command.

This kept messing up the requests state to the server.

When I changed the manifest command to start sending 5 bytes and getState to use 5 bytes per requests, every command was interpreted as intended state and responded with accurate values.

Thanks for pitching your ideas and thoughts on the issue team!

3 Likes