diff --git a/android/app/src/main/kotlin/com/mag160c/thermal/usb/MagProtocol.kt b/android/app/src/main/kotlin/com/mag160c/thermal/usb/MagProtocol.kt index b51aebe..6274922 100644 --- a/android/app/src/main/kotlin/com/mag160c/thermal/usb/MagProtocol.kt +++ b/android/app/src/main/kotlin/com/mag160c/thermal/usb/MagProtocol.kt @@ -1,7 +1,5 @@ package com.mag160c.thermal.usb -import java.nio.ByteBuffer - /** * Vendor command/response protocol — VERBATIM from the official MAG-Cx app's * Java source (sdk/UsbCommunication.java + P2DCmd.java/D2PCmd.java, jadx). @@ -33,17 +31,25 @@ object MagProtocol { const val RSP_SEND_CALI_FILE = 0x5BB5B55D const val RSP_SEND_CALI_INFO = 0x5BB5B55E - fun cmd4(magic: Int): ByteArray { - val b = ByteBuffer.allocate(4) - b.putInt(magic) - return b.array() - } + /** + * 4-byte LITTLE-ENDIAN command — byte-identical to the official app's + * GlobalFunc.intToByteArray (a&255 first). CRITICAL: ByteBuffer.putInt + * defaults to BIG_ENDIAN and once reversed every non-palindrome magic + * (66c/66f/670/672/673/674) — the camera only understood the palindrome + * 0x6BB6B66B, which masked this for 16 rounds. + */ + fun cmd4(magic: Int): ByteArray = byteArrayOf( + (magic and 0xFF).toByte(), + ((magic shr 8) and 0xFF).toByte(), + ((magic shr 16) and 0xFF).toByte(), + ((magic shr 24) and 0xFF).toByte(), + ) + /** 8-byte {u32 magic LE, u32 param LE} (official SetShutterState packet). */ fun cmd8(magic: Int, param: Int): ByteArray { - val b = ByteBuffer.allocate(8) - b.putInt(magic) - b.putInt(param) - return b.array() + val lo = cmd4(magic) + val hi = cmd4(param) + return byteArrayOf(lo[0], lo[1], lo[2], lo[3], hi[0], hi[1], hi[2], hi[3]) } /** Camera info block (0x5BB5B55B, 0x38 bytes): +0x00 pid, +0x08 serial, diff --git a/android/app/src/test/kotlin/com/mag160c/thermal/usb/MagProtocolTest.kt b/android/app/src/test/kotlin/com/mag160c/thermal/usb/MagProtocolTest.kt new file mode 100644 index 0000000..2f08775 --- /dev/null +++ b/android/app/src/test/kotlin/com/mag160c/thermal/usb/MagProtocolTest.kt @@ -0,0 +1,43 @@ +package com.mag160c.thermal.usb + +import org.junit.Assert.assertEquals +import org.junit.Test + +/** + * Wire-format regression lock: commands MUST be little-endian, byte-identical + * to the official MAG-Cx app (GlobalFunc.intToByteArray: a&255 goes first). + * ByteBuffer.putInt defaults to BIG_ENDIAN and once reversed every + * non-palindrome command magic — the camera answered only 0x6BB6B66B (a + * palindrome) which hid the bug for 16 rounds of black-screen debugging. + */ +class MagProtocolTest { + @Test + fun cmd4IsLittleEndian() { + // official: intToByteArray(1807136364) == {6C, B6, B6, 6B} + val b = MagProtocol.cmd4(0x6BB6B66C) + assertEquals(0x6C.toByte(), b[0]) + assertEquals(0xB6.toByte(), b[1]) + assertEquals(0xB6.toByte(), b[2]) + assertEquals(0x6B.toByte(), b[3]) + } + + @Test + fun cmd8MatchesOfficialSetShutterStatePacket() { + // official setShutterState_(on): {72, B6, B6, 6B, on, on>>8, on>>16, on>>24} + val b = MagProtocol.cmd8(0x6BB6B672, 1) + assertEquals(0x72.toByte(), b[0]) + assertEquals(0xB6.toByte(), b[1]) + assertEquals(0xB6.toByte(), b[2]) + assertEquals(0x6B.toByte(), b[3]) + assertEquals(0x01.toByte(), b[4]) + assertEquals(0x00.toByte(), b[5]) + assertEquals(0x00.toByte(), b[6]) + assertEquals(0x00.toByte(), b[7]) + } + + @Test + fun startTransferImgPacketMatchesOfficial() { + val b = MagProtocol.cmd4(0x6BB6B673) + assertEquals(listOf(0x73, 0xB6.toByte(), 0xB6.toByte(), 0x6B), b.toList()) + } +} diff --git a/build-artifacts/mag160c-app-debug.apk b/build-artifacts/mag160c-app-debug.apk index 2048940..ef93ce0 100644 --- a/build-artifacts/mag160c-app-debug.apk +++ b/build-artifacts/mag160c-app-debug.apk @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:03a3c8485f6c936695b27fbca7902e5760807326625883a55b3bc1f159a00e54 +oid sha256:38cbe556fadf9d05d827cfce7e670ff95bfa4e58ef6daef5e4ac738b6c5673c4 size 11873886