81 lines
3.0 KiB
Java
81 lines
3.0 KiB
Java
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;
|
|
}
|
|
}
|
|
}
|