Files
MicrOBU/app/src/test/java/com/hawhamburg/micr0bu/StationLinkTest.kt
T

120 lines
5.3 KiB
Kotlin
Raw Normal View History

package com.hawhamburg.micr0bu
import com.hawhamburg.micr0bu.data.transport.BtpDataRequest
import com.hawhamburg.micr0bu.data.transport.LinkMessage
import com.hawhamburg.micr0bu.data.transport.LinkOpcode
import com.hawhamburg.micr0bu.data.transport.LinkResult
import com.hawhamburg.micr0bu.data.transport.LinkSecurityProfile
import com.hawhamburg.micr0bu.data.transport.PotiUpdate
import com.hawhamburg.micr0bu.data.transport.StationConfigure
import com.hawhamburg.micr0bu.data.transport.StationInfo
import com.hawhamburg.micr0bu.data.transport.StationStatus
import com.hawhamburg.micr0bu.data.transport.credentialsSegment
import org.junit.Assert.assertArrayEquals
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Test
/**
* Pins the phone side of the station-link message layer.
*
* ## Where the expected bytes come from
* The colleague's Python implementation of the same protocol, microbu-esp32c5
* station-link/python/microbu_link/messages.py (`encode_message` over each body's `encode`),
* run with the same field values. That module is what their phone emulator drives the firmware
* with, so an agreement here is agreement with a second, independent implementation.
*/
class StationLinkTest {
private fun String.hexToBytes(): ByteArray = chunked(2).map { it.toInt(16).toByte() }.toByteArray()
private fun ByteArray.hex(): String = joinToString("") { "%02x".format(it) }
@Test
fun `STATION_CONFIGURE matches the Python encoder`() {
val body = StationConfigure(stationType = 2, mid = "021122334455".hexToBytes()).encode()
assertEquals(
"0100070002010002112233445500b40014020205",
LinkMessage(LinkOpcode.STATION_CONFIGURE, 7, body).encode().hex(),
)
}
@Test
fun `POTI_UPDATE matches the Python encoder`() {
val body = PotiUpdate(
timestampMs = 717_254_800_123L, latTenMicroDeg = 535_546_667, lonTenMicroDeg = 100_223_889,
semiMajorCm = 486, semiMinorCm = 486, speedCms = 543, headingDeciDeg = 1234, pai = true,
).encode()
assertEquals(
"02000800fbb2b7ffa60000002bcbeb1f914bf905e601e60100000e000000001f02d204",
LinkMessage(LinkOpcode.POTI_UPDATE, 8, body).encode().hex(),
)
}
@Test
fun `BTP_DATA_REQUEST for a signed CAM matches the Python encoder`() {
val body = BtpDataRequest(
destinationPort = 2001, itsAid = 36, securityProfile = LinkSecurityProfile.SECURED,
permissions = "010000".hexToBytes(), flSdu = "0102030405".hexToBytes(),
).encode()
assertEquals(
"0300090001d1070000010102ffff000000000024000000030100000005000102030405",
LinkMessage(LinkOpcode.BTP_DATA_REQUEST, 9, body).encode().hex(),
)
}
@Test
fun `CREDENTIALS_PROVISION segment matches the Python encoder`() {
val body = credentialsSegment(totalLength = 695, offset = 240, segment = ByteArray(3) { 0xAB.toByte() })
assertEquals("04000a00b702f00003ababab", LinkMessage(LinkOpcode.CREDENTIALS_PROVISION, 10, body).encode().hex())
}
@Test
fun `STATUS from the Python encoder decodes field by field`() {
val message = LinkMessage.decode(
("8400341240e20100010800021122334455b80b49387a4c12eb00010b0000000c0000000d0000000e000000" +
"0f000000100000001100000012000000130000001400000015000000160000001700000018000000" +
"190000001a0000001b0000001c000000fbb2b7ffa6000000").hexToBytes(),
)
assertNotNull(message)
assertEquals(LinkOpcode.STATUS, message!!.opcode)
assertEquals(0x1234, message.sequence)
val status = StationStatus.decode(message.body)!!
assertEquals(123_456L, status.uptimeMs)
assertTrue(status.configured)
assertArrayEquals("b80b49387a4c12eb".hexToBytes(), status.identifier)
assertEquals(1, status.tickets)
assertEquals(11L, status.signedMessages)
assertEquals(12L, status.refusedNoTicket)
assertEquals(13L, status.refusedChangePending)
assertEquals(14L, status.refusedPermission)
assertEquals(15L, status.signFailed)
assertEquals(16L, status.verified)
assertEquals(17L, status.rejected)
assertEquals(18L, status.requestsAccepted)
assertEquals(19L, status.requestsRefused)
assertEquals(21L, status.radioSubmitted)
assertEquals(22L, status.radioFailed)
assertEquals(23L, status.radioReceived)
assertEquals(24L, status.radioDropped)
assertEquals(26L, status.linkCrcErrors)
assertEquals(27L, status.linkMalformed)
assertEquals(28L, status.potiUpdates)
assertEquals(717_254_800_123L, status.itsTimeMs)
}
@Test
fun `a STATUS of the wrong length is refused, as the Python decoder does`() {
assertNull(StationStatus.decode(ByteArray(StationStatus.SIZE + 1)))
}
@Test
fun `RESULT of STATION_CONFIGURE carries the credential state`() {
val message = LinkMessage.decode("8000070000120800021122334455b80b49387a4c12eb0101".hexToBytes())!!
val result = LinkResult.decode(message.body)!!
assertTrue(result.accepted)
assertEquals(StationInfo(credentialsLoaded = true, tickets = 1), StationInfo.decode(result.detail))
}
}