120 lines
5.3 KiB
Kotlin
120 lines
5.3 KiB
Kotlin
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))
|
||
|
|
}
|
||
|
|
}
|