feat: publish NIRO Gen5W research overview and reviewed source examples

This commit is contained in:
seo committed 2026-11-02 09:59:38 +09:00
commit ccad0ed9f3
23 files changed
+1279

No files matched your search

+96
View File
@@ -0,0 +1,96 @@
package com.chaegeon.niro;
import java.io.FileInputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public final class HscanRpmDecoderTest {
private static int assertions;
private static final class Sink implements HscanRpmDecoder.Listener {
final List<Integer> values = new ArrayList<Integer>();
public void onRpm(int rpm) { values.add(rpm); }
}
public static void main(String[] args) throws Exception {
Sink sink = new Sink();
HscanRpmDecoder decoder = new HscanRpmDecoder(sink);
byte[] zero = packet(0), rev = packet(8403);
decoder.accept(zero, 0, zero.length);
check(sink.values.equals(Arrays.asList(0)), "valid stopped engine");
for (byte b : rev) decoder.accept(new byte[] {b}, 0, 1);
check(sink.values.equals(Arrays.asList(0, 2100)), "single-byte fragmentation + stock truncation");
byte[] burst = new byte[528];
System.arraycopy(packet(9200), 0, burst, 0, 264);
System.arraycopy(packet(6400), 0, burst, 264, 264);
decoder.accept(burst, 0, burst.length);
check(sink.values.equals(Arrays.asList(0, 2100, 2300, 1600)), "coalesced changes emitted before accept returns");
int before = sink.values.size();
byte[] bad = packet(4000); bad[30] ^= 1;
decoder.accept(bad, 0, bad.length);
check(sink.values.size() == before, "bad XOR never emitted");
bad = packet(4000); bad[9] = 2; bad[22] = 9; checksum(bad);
decoder.accept(bad, 0, bad.length);
check(sink.values.size() == before, "validate entire frame before any callback");
bad = packet(4000); bad[12] = 7; checksum(bad);
decoder.accept(bad, 0, bad.length);
check(sink.values.size() == before, "wrong EMS1 length");
bad = packet(4000); bad[200] = 1; checksum(bad);
decoder.accept(bad, 0, bad.length);
check(sink.values.size() == before, "nonzero tail rejected");
bad = packet(4000); bad[9] = (byte) 255; checksum(bad);
decoder.accept(bad, 0, bad.length);
check(sink.values.size() == before, "excess count bounded");
bad = packet(4000); bad[11] = 0x17; checksum(bad);
decoder.accept(bad, 0, bad.length);
check(sink.values.size() == before, "unrelated CAN not RPM");
bad = packet(4000); bad[2] = 0; checksum(bad);
decoder.accept(bad, 0, bad.length);
check(sink.values.size() == before, "other envelope not accepted");
for (int sentinel : new int[] {65534, 65535}) {
byte[] bytes = packet(sentinel); decoder.accept(bytes, 0, bytes.length);
}
check(sink.values.size() == before, "reserved raw values ignored");
decoder.accept(new byte[] {1, 2, (byte) 255, 4, 5}, 0, 5);
decoder.accept(rev, 0, rev.length);
check(sink.values.size() == before + 1, "recovery after corrupt frames and garbage");
for (int split = 1; split < 264; split++) {
Sink s = new Sink(); HscanRpmDecoder d = new HscanRpmDecoder(s);
d.accept(rev, 0, split);
check(s.values.isEmpty(), "partial frame held");
d.accept(rev, split, rev.length - split);
check(s.values.equals(Arrays.asList(2100)), "every split decodes once");
}
if (args.length > 0) {
Sink live = new Sink(); HscanRpmDecoder d = new HscanRpmDecoder(live);
long bytes = 0;
for (String file : args) {
FileInputStream in = new FileInputStream(file);
try {
byte[] buffer = new byte[137]; int n;
while ((n = in.read(buffer)) >= 0) { d.accept(buffer, 0, n); bytes += n; }
} finally { in.close(); }
}
check(d.validFrames * 264 == bytes, "all private capture frames validated");
check(d.rejectedFrames == 0 && d.samples > 500, "real EMS1 samples recovered");
System.out.println("private_fixture_frames=" + d.validFrames + " rpm_samples=" + d.samples);
}
System.out.println("HscanRpmDecoderTest OK assertions=" + assertions);
}
private static byte[] packet(int raw) {
byte[] b = new byte[264];
int[] header = {255, 11, 142, 1, 139, 199, 1, 0, 199, 1, 0, 22, 8};
for (int i = 0; i < header.length; i++) b[i] = (byte) header[i];
b[15] = (byte) raw; b[16] = (byte) (raw >> 8);
checksum(b); return b;
}
private static void checksum(byte[] b) {
b[263] = 0;
for (int i = 0; i < 263; i++) b[263] ^= b[i];
}
private static void check(boolean good, String message) {
assertions++; if (!good) throw new AssertionError(message);
}
}
@@ -0,0 +1,65 @@
package com.chaegeon.niro;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
public final class WeatherRefreshCountersTest {
private static int checks;
private static void ok(boolean actual) {
++checks;
if (!actual) { throw new AssertionError("check " + checks); }
}
private static String read(File file) throws Exception {
FileInputStream input = new FileInputStream(file);
try {
byte[] data = new byte[32];
int length = input.read(data);
ok(length > 0 && input.read() == -1);
return new String(data, 0, length, "US-ASCII");
} finally {
input.close();
}
}
public static void main(String[] args) throws Exception {
File directory = File.createTempFile("weather-refresh-counters-", ".dir");
if (!directory.delete() || !directory.mkdir()) {
throw new IOException("test directory setup failed");
}
File signal = new File(directory, "signal.counter");
File callback = new File(directory, "callback.counter");
try {
WeatherRefreshCounters counters = new WeatherRefreshCounters(directory);
ok("0\n".equals(read(signal)));
ok("0\n".equals(read(callback)));
counters.signalAccepted();
counters.signalAccepted();
ok("2\n".equals(read(signal)));
ok("0\n".equals(read(callback)));
counters.freshCallbackCompleted();
ok("2\n".equals(read(signal)));
ok("1\n".equals(read(callback)));
ok(!new File(directory, "signal.counter.new").exists());
ok(!new File(directory, "callback.counter.new").exists());
File stale = new File(directory, "signal.counter.new");
ok(stale.createNewFile());
boolean failedClosed = false;
try {
counters.signalAccepted();
} catch (IOException expected) {
failedClosed = true;
}
ok(failedClosed);
ok("2\n".equals(read(signal)));
ok(stale.delete());
} finally {
if (signal.exists() && !signal.delete()) { throw new IOException("signal cleanup"); }
if (callback.exists() && !callback.delete()) { throw new IOException("callback cleanup"); }
if (!directory.delete()) { throw new IOException("directory cleanup"); }
}
System.out.println("result=PASS_WEATHER_REFRESH_COUNTERS checks=" + checks);
}
}
+59
View File
@@ -0,0 +1,59 @@
import com.chaegeon.niro.WeatherRefreshGate;
public final class WeatherRefreshGateTest {
private static void check(boolean condition, String name) {
if (!condition) {
throw new AssertionError(name);
}
}
public static void main(String[] args) {
WeatherRefreshGate gate = new WeatherRefreshGate(60000L, 10000L, 2000L);
check(gate.admit(100L) == WeatherRefreshGate.Admission.FETCH,
"first fetch accepted");
check(gate.isRunning(), "running");
check(gate.admit(101L) == WeatherRefreshGate.Admission.COALESCE,
"in-flight request coalesced");
gate.finish(200L, WeatherRefreshGate.Admission.FETCH, true);
check(!gate.isRunning(), "finished");
check(gate.admit(300L) == WeatherRefreshGate.Admission.REPLAY,
"recent successful response replayed");
check(gate.replayDelayMillis(300L) == 1900L,
"first replay waits until two seconds after fetch callback");
check(gate.admit(301L) == WeatherRefreshGate.Admission.COALESCE,
"in-flight replay coalesced");
gate.finish(400L, WeatherRefreshGate.Admission.REPLAY, true);
check(gate.admit(500L) == WeatherRefreshGate.Admission.REPLAY,
"second replay admitted without new fetch");
check(gate.replayDelayMillis(500L) == 1900L,
"subsequent replay is paced from previous callback");
gate.finish(501L, WeatherRefreshGate.Admission.REPLAY, true);
check(gate.admit(60199L) == WeatherRefreshGate.Admission.REPLAY,
"replay does not extend fetch TTL");
check(gate.replayDelayMillis(60199L) == 0L,
"replay runs immediately after spacing elapsed");
gate.finish(60200L, WeatherRefreshGate.Admission.REPLAY, true);
check(gate.admit(60200L) == WeatherRefreshGate.Admission.FETCH,
"new fetch accepted at original TTL boundary");
gate.finish(60300L, WeatherRefreshGate.Admission.FETCH, false);
check(gate.admit(60301L) == WeatherRefreshGate.Admission.BACKOFF,
"fast failed-fetch retry is throttled");
check(!gate.isRunning(), "backoff does not start worker");
check(gate.admit(70299L) == WeatherRefreshGate.Admission.BACKOFF,
"retry stays throttled until boundary");
check(gate.admit(70300L) == WeatherRefreshGate.Admission.FETCH,
"failed fetch may retry after ten seconds");
gate.finish(70302L, WeatherRefreshGate.Admission.FETCH, true);
check(gate.admit(70303L) == WeatherRefreshGate.Admission.REPLAY,
"successful response becomes replayable");
gate.finish(70304L, WeatherRefreshGate.Admission.REPLAY, false);
check(gate.admit(70305L) == WeatherRefreshGate.Admission.BACKOFF,
"failed replay invalidates cache and throttles retry");
check(gate.admit(80303L) == WeatherRefreshGate.Admission.BACKOFF,
"failed replay still throttled before retry boundary");
check(gate.admit(80304L) == WeatherRefreshGate.Admission.FETCH,
"failed replay retries with fresh fetch after ten seconds");
gate.finish(80306L, WeatherRefreshGate.Admission.FETCH, false);
System.out.println("result=PASS_WEATHER_REFRESH_GATE");
}
}
+33
View File
@@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/../../backend/php/NiroMqttKeepalive.php';
function assertMqttKeepalive(bool $condition, string $message): void
{
if (!$condition) {
fwrite(STDERR, "NiroMqttKeepaliveTest failed: {$message}\n");
exit(1);
}
}
$keepalive = new NiroMqttKeepalive(100, 15);
for ($now = 101; $now < 115; $now++) {
assertMqttKeepalive(!$keepalive->isDue($now), 'PINGREQ became due too early');
}
assertMqttKeepalive($keepalive->isDue(115),
'continuous inbound packets postponed the client PINGREQ');
$keepalive->markSent(115);
assertMqttKeepalive(!$keepalive->isDue(129), 'PINGREQ repeated before its interval');
assertMqttKeepalive($keepalive->isDue(130), 'next PINGREQ was not scheduled');
$invalidIntervalRejected = false;
try {
new NiroMqttKeepalive(100, 0);
} catch (InvalidArgumentException) {
$invalidIntervalRejected = true;
}
assertMqttKeepalive($invalidIntervalRejected, 'invalid keepalive interval was accepted');
echo "NiroMqttKeepaliveTest passed\n";
+64
View File
@@ -0,0 +1,64 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/../../backend/php/NiroRawSnappy.php';
function checkSnappy(bool $condition, string $message): void
{
if (!$condition) {
throw new RuntimeException($message);
}
}
/** Independent literal-only reader for synthetic encoder tests. */
function readLiteralSnappy(string $encoded): string
{
$position = 0;
$length = 0;
$shift = 0;
do {
checkSnappy($position < strlen($encoded) && $shift <= 28, 'invalid varint');
$byte = ord($encoded[$position++]);
$length |= ($byte & 0x7f) << $shift;
$shift += 7;
} while (($byte & 0x80) !== 0);
$output = '';
while ($position < strlen($encoded)) {
$tag = ord($encoded[$position++]);
checkSnappy(($tag & 3) === 0, 'non-literal tag');
$size = $tag >> 2;
if ($size >= 60) {
$count = $size - 59;
checkSnappy($position + $count <= strlen($encoded), 'truncated size');
$size = 0;
for ($i = 0; $i < $count; ++$i) {
$size |= ord($encoded[$position++]) << ($i * 8);
}
}
++$size;
checkSnappy($position + $size <= strlen($encoded), 'truncated literal');
$output .= substr($encoded, $position, $size);
$position += $size;
}
checkSnappy(strlen($output) === $length, 'uncompressed length mismatch');
return $output;
}
$cases = ['', 'a', "synthetic\0payload"];
foreach ([60, 61, 255, 256, 65535, 65536, 65537, 131073] as $length) {
$cases[] = str_repeat('x', $length);
}
foreach ($cases as $plain) {
$encoded = NiroRawSnappy::encodeLiteral($plain);
checkSnappy(readLiteralSnappy($encoded) === $plain, 'round trip failed');
checkSnappy($encoded === NiroRawSnappy::encodeLiteral($plain), 'not deterministic');
}
$rejected = false;
try {
NiroRawSnappy::encodeLiteral(str_repeat('x', 8 * 1024 * 1024 + 1));
} catch (InvalidArgumentException $expected) {
$rejected = true;
}
checkSnappy($rejected, 'oversized input was accepted');
echo 'NiroRawSnappyTest passed cases=' . count($cases) . PHP_EOL;
+78
View File
@@ -0,0 +1,78 @@
"""Synthetic structural tests only; no OEM image or ARM execution is used."""
import hashlib
import importlib.util
from pathlib import Path
import unittest
from unittest.mock import patch
ROOT = Path(__file__).resolve().parents[2]
PATH = ROOT / "navigation/archived/v038/patch_nav_vehicle_speed_source.py"
SPEC = importlib.util.spec_from_file_location("archived_speed_patch", PATH)
MODULE = importlib.util.module_from_spec(SPEC)
SPEC.loader.exec_module(MODULE)
class ArchivedSpeedPatchTests(unittest.TestCase):
def fixture(self):
settings = dict(
SETTER_OFFSET=16,
SETTER_CONTINUE=20,
CAVE_OFFSET=64,
CAVE_SIZE=60,
DISPLAY_CALL_SITES=(160, 168, 176, 184, 192),
CONVERT_SPEED=256,
SAFETY_CALL_SITE=200,
)
image = bytearray(320)
image[16:20] = MODULE.SETTER_ORIGINAL
for site in settings["DISPLAY_CALL_SITES"] + (settings["SAFETY_CALL_SITE"],):
image[site:site + 4] = MODULE.encode_thumb_branch(site, 256, link=True)
settings["SOURCE_SHA256"] = hashlib.sha256(image).hexdigest()
return bytes(image), settings
def test_unknown_original_is_rejected(self):
with self.assertRaisesRegex(ValueError, "unknown source"):
MODULE.patch_image(b"synthetic-not-a-library")
def test_thumb_alignment_and_range_are_checked(self):
self.assertEqual(MODULE.encode_thumb_branch(0x1000, 0x1004), b"\x00\xf0\x00\xb8")
with self.assertRaisesRegex(ValueError, "halfword"):
MODULE.encode_thumb_branch(0x1000, 0x1005)
with self.assertRaisesRegex(ValueError, "out of range"):
MODULE.encode_thumb_branch(0x1000, 0x2001004)
def test_only_declared_windows_change(self):
original, settings = self.fixture()
with patch.multiple(MODULE, **settings):
result = MODULE.patch_image(original)
allowed = set(range(16, 20)) | set(range(64, 124))
changed = {i for i, (a, b) in enumerate(zip(original, result)) if a != b}
self.assertTrue(changed)
self.assertLessEqual(changed, allowed)
self.assertEqual(len(result), len(original))
for site in settings["DISPLAY_CALL_SITES"] + (settings["SAFETY_CALL_SITE"],):
self.assertEqual(result[site:site + 4], original[site:site + 4])
def test_occupied_cave_is_rejected_even_with_matching_hash(self):
original, settings = self.fixture()
bad = bytearray(original)
bad[64] = 1
settings["SOURCE_SHA256"] = hashlib.sha256(bad).hexdigest()
with patch.multiple(MODULE, **settings):
with self.assertRaisesRegex(ValueError, "code cave"):
MODULE.patch_image(bytes(bad))
def test_changed_safety_call_is_rejected(self):
original, settings = self.fixture()
bad = bytearray(original)
bad[200] ^= 1
settings["SOURCE_SHA256"] = hashlib.sha256(bad).hexdigest()
with patch.multiple(MODULE, **settings):
with self.assertRaisesRegex(ValueError, "overspeed"):
MODULE.patch_image(bytes(bad))
if __name__ == "__main__":
unittest.main()