package com.chaegeon.niro; /** Receive-only V038 Can2A20HEV ALL_C / EMS1 decoder. No Android or transport I/O. */ final class HscanRpmDecoder { interface Listener { void onRpm(int rpm); } // Observed hscand envelope: FF 0B 8E 01 8B C7 01 00, 255 data bytes + XOR. private static final int FRAME_SIZE = 264; private static final int[] HEADER = {255, 11, 142, 1, 139, 199, 1, 0}; private final byte[] frame = new byte[FRAME_SIZE]; private final Listener listener; private int used; long validFrames; long rejectedFrames; long samples; HscanRpmDecoder(Listener listener) { this.listener = listener; } void accept(byte[] bytes, int offset, int length) { if (offset < 0 || length < 0 || offset > bytes.length - length) throw new IndexOutOfBoundsException(); for (int i = offset; i < offset + length; i++) { frame[used++] = bytes[i]; // Find the exact bounded header again after garbage or corrupt framing. while (used > 0 && !headerMatches()) discardFirst(); if (used == FRAME_SIZE) { if (validate()) { validFrames++; emit(); used = 0; } else { rejectedFrames++; discardFirst(); while (used > 0 && !headerMatches()) discardFirst(); } } } } private boolean headerMatches() { for (int i = 0; i < Math.min(used, HEADER.length); i++) if ((frame[i] & 255) != HEADER[i]) return false; return true; } private void discardFirst() { System.arraycopy(frame, 1, frame, 0, --used); } private boolean validate() { int xor = 0; for (byte value : frame) xor ^= value & 255; if (xor != 0 || (frame[8] & 255) != 0xc7) return false; int count = (frame[9] & 255) | ((frame[10] & 255) << 8); int end = FRAME_SIZE - 1, p = 11; if (count > (end - p) / 3) return false; for (int i = 0; i < count; i++) { if (p + 2 > end) return false; int fn = frame[p++] & 255, size = frame[p++] & 255; if (size < 1 || size > 8 || p + size > end) return false; // A different profile must never reinterpret a short record as EMS1. if (fn == 0x16 && size != 8) return false; p += size; } while (p < end) if (frame[p++] != 0) return false; return true; } private void emit() { int count = (frame[9] & 255) | ((frame[10] & 255) << 8), p = 11; for (int i = 0; i < count; i++) { int fn = frame[p++] & 255, size = frame[p++] & 255; if (fn == 0x16) { // CAN 0x316 EMS1: same LE16 * 0.25 and int truncation as stock HEV. int raw = (frame[p + 2] & 255) | ((frame[p + 3] & 255) << 8); if (raw < 0xfffe) { samples++; listener.onRpm(raw / 4); } } p += size; } } }