package com.gronod.esp32aldldashboard import com.gronod.esp32aldldashboard.parser.ALDLParser import org.junit.Assert.* import org.junit.Test class ALDLParserTest { @Test fun testParseSampleFrame() { // Sample hex stream from user request: // AA 55 20 00 2A 5F 59 00 F4 00 1E 80 65 08 00 00 00 25 18 7D 80 00 00 00 C9 02 62 // AA 55 is the header, followed by 25 bytes of payload. val rawPayload = byteArrayOf( 0x20.toByte(), 0x00.toByte(), 0x2A.toByte(), 0x5F.toByte(), 0x59.toByte(), 0x00.toByte(), 0xF4.toByte(), 0x00.toByte(), 0x1E.toByte(), 0x80.toByte(), 0x65.toByte(), 0x08.toByte(), 0x00.toByte(), 0x00.toByte(), 0x00.toByte(), 0x25.toByte(), 0x18.toByte(), 0x7D.toByte(), 0x80.toByte(), 0x00.toByte(), 0x00.toByte(), 0x00.toByte(), 0xC9.toByte(), 0x02.toByte(), 0x62.toByte() ) val result = ALDLParser.parseFrame(rawPayload) assertTrue(result is com.gronod.esp32aldldashboard.parser.ALDLParseResult.Success) val frame = (result as com.gronod.esp32aldldashboard.parser.ALDLParseResult.Success).frame // Assert values based on 24-INT10.ads specifications: assertEquals(95, frame.iacPosition) // u[3] (Byte 4) // coolantTemp = 89 * 0.75 - 40 = 26.75 assertEquals(26.75f, frame.coolantTempC, 0.001f) assertEquals(80.15f, frame.coolantTempF, 0.001f) assertEquals(0, frame.vehicleSpeedMPH) // u[5] (Byte 6) // MAP: u[6] = 244 assertEquals(244 * 0.019608f, frame.mapVolts, 0.001f) assertEquals(244 * 0.369f + 10.354f, frame.mapKpa, 0.001f) assertEquals(0, frame.engineSpeedRpm) // u[7] (Byte 8) // TPS: u[8] = 30 assertEquals(30 * 0.019608f, frame.tpsVolts, 0.001f) assertEquals(128, frame.integrator) // u[9] (Byte 10) assertEquals(101 * 4.44f, frame.o2SensorMv, 0.001f) // u[10] (Byte 11) assertEquals(12.5f, frame.batteryVolts, 0.001f) // u[17] (Byte 18) assertEquals(128, frame.blm) // u[18] (Byte 19) assertEquals(0, frame.richLeanCrosses) // u[19] (Byte 20) assertEquals(0f, frame.sparkAdvance, 0.001f) // u[20] (Byte 21) assertEquals(0f, frame.egrDutyCycle, 0.001f) // u[21] (Byte 22) // MAT Temp: u[22] = 201 (decimal) // Table 52 interpolation: // Key 199 -> 20.0 C // Key 209 -> 15.0 C // value = 20.0 - (201 - 199) / 10.0 * 5.0 = 19.0 C assertEquals(19.0f, frame.matC, 0.001f) assertEquals(66.2f, frame.matF, 0.001f) // BPW: u[23]=0x02, u[24]=0x62 => 0x0262 = 610 decimal // bpwMs = 610 * 0.015259 = 9.30799 assertEquals(610 * 0.015259f, frame.bpwMs, 0.001f) // Active fault codes check (0x08 on codesByte1 = Code 21 active) assertTrue(frame.activeFaultCodes.contains(21)) assertEquals(1, frame.activeFaultCodes.size) // Misc flags assertFalse(frame.isClosedLoop) // bit 7 of u[14] is 0 assertFalse(frame.isRich) // bit 6 of u[14] is 0 } @Test fun testRpmBoundaryValues() { // Max representable RPM: 6375 (255 * 25) val validPayload = createBasePayload().apply { this[7] = 255.toByte() // 255 * 25 = 6375 RPM } val validResult = ALDLParser.parseFrame(validPayload) assertTrue("Max representable RPM should be valid", validResult is com.gronod.esp32aldldashboard.parser.ALDLParseResult.Success) } @Test fun testBatteryVoltageBoundaryValues() { // Min valid: 8V (80 * 0.1 = 8.0V) val minPayload = createBasePayload().apply { this[17] = 80.toByte() } val minResult = ALDLParser.parseFrame(minPayload) assertTrue("Battery at 8V should be valid", minResult is com.gronod.esp32aldldashboard.parser.ALDLParseResult.Success) // Max valid: 18V (180 * 0.1 = 18.0V) val maxPayload = createBasePayload().apply { this[17] = 180.toByte() } val maxResult = ALDLParser.parseFrame(maxPayload) assertTrue("Battery at 18V should be valid", maxResult is com.gronod.esp32aldldashboard.parser.ALDLParseResult.Success) // Too low: 4.9V (49 * 0.1 = 4.9V, below 5V minimum) val lowPayload = createBasePayload().apply { this[17] = 49.toByte() } val lowResult = ALDLParser.parseFrame(lowPayload) assertTrue("Battery below 5V should be rejected", lowResult is com.gronod.esp32aldldashboard.parser.ALDLParseResult.InvalidData) // Too high: 20.1V (201 * 0.1 = 20.1V, above 20V maximum) val highPayload = createBasePayload().apply { this[17] = 201.toByte() } val highResult = ALDLParser.parseFrame(highPayload) assertTrue("Battery above 20V should be rejected", highResult is com.gronod.esp32aldldashboard.parser.ALDLParseResult.InvalidData) } @Test fun testTpsVoltageBoundaryValues() { // Max representable TPS: 5.0V (255 * 0.019608) val validPayload = createBasePayload().apply { this[8] = 255.toByte() } val validResult = ALDLParser.parseFrame(validPayload) assertTrue("Max representable TPS should be valid", validResult is com.gronod.esp32aldldashboard.parser.ALDLParseResult.Success) } @Test fun testCoolantTempBoundaryValues() { // Valid temperature: 89 * 0.75 - 40 = 26.75C (within -45 to 220 range) val validPayload = createBasePayload() val validResult = ALDLParser.parseFrame(validPayload) assertTrue("Valid coolant temp should be accepted", validResult is com.gronod.esp32aldldashboard.parser.ALDLParseResult.Success) // Max representable: 255 * 0.75 - 40 = 151.25C val maxPayload = createBasePayload().apply { this[4] = 255.toByte() } val maxResult = ALDLParser.parseFrame(maxPayload) assertTrue("Max representable coolant temp should be accepted", maxResult is com.gronod.esp32aldldashboard.parser.ALDLParseResult.Success) // Min representable: 0 * 0.75 - 40 = -40C val minPayload = createBasePayload().apply { this[4] = 0.toByte() } val minResult = ALDLParser.parseFrame(minPayload) assertTrue("Min representable coolant temp should be accepted", minResult is com.gronod.esp32aldldashboard.parser.ALDLParseResult.Success) } @Test fun testIncompletePayload() { // Payload with only 24 bytes (incomplete) val incompletePayload = ByteArray(24) { 0x20.toByte() } val result = ALDLParser.parseFrame(incompletePayload) assertTrue("Incomplete payload should return Incomplete result", result is com.gronod.esp32aldldashboard.parser.ALDLParseResult.Incomplete) } private fun createBasePayload(): ByteArray { return byteArrayOf( 0x20.toByte(), 0x00.toByte(), 0x2A.toByte(), 0x5F.toByte(), 0x59.toByte(), 0x00.toByte(), 0xF4.toByte(), 0x00.toByte(), 0x1E.toByte(), 0x80.toByte(), 0x65.toByte(), 0x08.toByte(), 0x00.toByte(), 0x00.toByte(), 0x00.toByte(), 0x25.toByte(), 0x18.toByte(), 0x7D.toByte(), 0x80.toByte(), 0x00.toByte(), 0x00.toByte(), 0x00.toByte(), 0xC9.toByte(), 0x02.toByte(), 0x62.toByte() ) } }