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)) } }