feat: publish NIRO Gen5W research overview and reviewed source examples
This commit is contained in:
commit
ccad0ed9f3
23 files changed
+1279
No files matched your search
@@ -0,0 +1,80 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user