mirror of
https://github.com/bod09/frame-autopass.git
synced 2026-10-06 00:00:05 +02:00
385 lines
18 KiB
C++
385 lines
18 KiB
C++
// Host-side tests for the sensor layer: XRService log parsing and the
|
|
// evidence rules (FINDINGS.md sections 29, 34 and 39), plus an end-to-end
|
|
// replay of the scenarios seen live, run through LightPolicy.
|
|
|
|
#include "light_policy.hpp"
|
|
#include "sensors.hpp"
|
|
#include "xrservice_log.hpp"
|
|
|
|
#include <cstdio>
|
|
#include <optional>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
using namespace autopass;
|
|
|
|
namespace {
|
|
|
|
int failures = 0;
|
|
#define CHECK(expr) do { if (!(expr)) { std::printf("FAIL line %d: %s\n", __LINE__, #expr); ++failures; } } while (0)
|
|
|
|
// Real XRService lines from 2026-09-30.
|
|
const char* kModeAuto = "Wed Sep 30 2026 22:39:15.067651 INFO: SLAMConsole: [IREmitters] IR emitters mode changed to Auto";
|
|
const char* kOn = "Wed Sep 30 2026 22:40:49.267834 INFO: SLAMConsole: [IREmitters] IR Emitters Turned On";
|
|
const char* kOff = "Wed Sep 30 2026 22:41:24.230440 INFO: SLAMConsole: [IREmitters] IR Emitters Turned Off";
|
|
const char* kNoise = "Wed Sep 30 2026 22:39:23.152963 WARNING: [DeckardCaptureSource] Max number of iteration reached when estimating the CCT from grey world gains";
|
|
|
|
void test_parsing() {
|
|
XrState s;
|
|
CHECK(apply_xrservice_line(kOn, &s) && s.emitters == std::optional<bool>(true));
|
|
CHECK(apply_xrservice_line(kOff, &s) && s.emitters == std::optional<bool>(false));
|
|
CHECK(apply_xrservice_line(kModeAuto, &s) && s.emitters == std::optional<bool>(false));
|
|
CHECK(!apply_xrservice_line(kNoise, &s));
|
|
CHECK(!apply_xrservice_line("IR Emitters Turned On", &s)); // must carry the [IREmitters] tag
|
|
|
|
// Real lines from 2026-09-30 for the other tracked facts.
|
|
CHECK(apply_xrservice_line("Wed Sep 30 2026 22:39:15.083409 INFO: [DeckardCaptureSource] Passthrough cameras resumed", &s));
|
|
CHECK(s.passthrough == std::optional<bool>(true));
|
|
CHECK(apply_xrservice_line("Wed Sep 30 2026 22:44:35.826667 INFO: [DeckardCaptureSource] Passthrough cameras paused", &s));
|
|
CHECK(s.passthrough == std::optional<bool>(false));
|
|
CHECK(apply_xrservice_line("Wed Sep 30 2026 22:44:35.792844 INFO: [UserPresence] Received onEnterStandby from SteamVR. Setting UserPresenceDetected to 0.", &s));
|
|
CHECK(s.standby == std::optional<bool>(true));
|
|
CHECK(apply_xrservice_line("Wed Sep 30 2026 22:49:17.425068 INFO: [UserPresence] Received onLeaveStandby from SteamVR. Setting UserPresenceDetected to 1.", &s));
|
|
CHECK(s.standby == std::optional<bool>(false));
|
|
CHECK(apply_xrservice_line("Wed Sep 30 2026 22:44:35.852332 INFO: Transition to IMUFallback. Latest tracked pose: ref=Cam0", &s));
|
|
CHECK(s.tracking_lost == std::optional<bool>(true));
|
|
CHECK(apply_xrservice_line("Wed Sep 30 2026 22:39:15.336452 INFO: [DCU] [IMUFallback] Disabled with latest prediction: ", &s));
|
|
CHECK(s.tracking_lost == std::optional<bool>(false));
|
|
// "Tracking cameras streaming paused" is not the passthrough line.
|
|
XrState t;
|
|
CHECK(!apply_xrservice_line("Wed Sep 30 2026 22:44:35.885592 INFO: [DeckardCaptureSource] Tracking cameras streaming paused", &t));
|
|
CHECK(!t.passthrough);
|
|
|
|
// Text: last line of each kind wins; a trailing partial line counts.
|
|
const std::string nl = "\n";
|
|
XrState u;
|
|
apply_xrservice_text(std::string(kModeAuto) + nl + kOn + nl + kNoise + nl + kOff + nl + kOn, &u);
|
|
CHECK(u.emitters == std::optional<bool>(true));
|
|
}
|
|
|
|
void test_ema() {
|
|
Ema e(1.0);
|
|
CHECK(!e.has());
|
|
CHECK(e.add(0, 10) == 10);
|
|
const double v = e.add(1000, 0); // one time constant later: ~37% left
|
|
CHECK(v > 3.5 && v < 3.9);
|
|
e.reset();
|
|
CHECK(!e.has());
|
|
}
|
|
|
|
void test_colour_rules() {
|
|
EvidenceBuilder b;
|
|
// Emitters on and colour dim: dark.
|
|
auto r = b.evaluate(0, Source::Color, true, 0.435);
|
|
CHECK(r.evidence == Evidence::Dark && r.cause == Cause::EmittersOnDim && !r.urgent);
|
|
// Emitters on but colour bright (a hand just left the cameras; the
|
|
// emitters lag ~5 s): not dark.
|
|
b.reset();
|
|
CHECK(b.evaluate(0, Source::Color, true, 0.95).evidence == Evidence::Neutral);
|
|
// Emitters off, dim-looking view in good light (the 23:27:47 case): not dark.
|
|
b.reset();
|
|
CHECK(b.evaluate(0, Source::Color, false, 0.63).evidence == Evidence::Neutral);
|
|
// Emitters off and colour reads dark (dusk, 2026-10-01 18:41): XRService's
|
|
// cameras have light enough, so no switch however long it lasts.
|
|
b.reset();
|
|
for (std::uint64_t t = 0; t <= 60000; t += 1000)
|
|
CHECK(b.evaluate(t, Source::Color, false, 0.01).evidence == Evidence::Neutral);
|
|
// Emitters on and no colour reading ever: dark.
|
|
b.reset();
|
|
CHECK(b.evaluate(0, Source::Color, true, std::nullopt).evidence == Evidence::Dark);
|
|
// Colour seen before but no fresh sample this tick: unknown.
|
|
b.reset();
|
|
b.evaluate(0, Source::Color, true, 0.9);
|
|
CHECK(b.evaluate(250, Source::Color, true, std::nullopt).evidence == Evidence::Unknown);
|
|
// Emitter state unknown: never switch either way.
|
|
b.reset();
|
|
CHECK(b.evaluate(0, Source::Color, std::nullopt, 0.0).evidence == Evidence::Neutral);
|
|
CHECK(b.evaluate(0, Source::Ir, std::nullopt, std::nullopt).evidence == Evidence::Neutral);
|
|
}
|
|
|
|
void test_ir_rules() {
|
|
EvidenceBuilder b;
|
|
auto r = b.evaluate(0, Source::Ir, false, std::nullopt);
|
|
CHECK(r.evidence == Evidence::Bright && r.cause == Cause::EmittersOff);
|
|
CHECK(b.evaluate(0, Source::Ir, true, std::nullopt).evidence == Evidence::Unknown);
|
|
|
|
// Emitters on and a dark IR light reading: not bright.
|
|
CHECK(b.evaluate(500, Source::Ir, true, std::nullopt, 0.0).evidence == Evidence::Neutral);
|
|
}
|
|
|
|
void test_ir_light() {
|
|
// The IR camera's own light value (FINDINGS.md 41): a lit reading is
|
|
// bright at once (it steps 0 -> 0.33 within 0.1 s; the policy's
|
|
// confirmation does the rest).
|
|
EvidenceBuilder a;
|
|
auto r = a.evaluate(0, Source::Ir, true, std::nullopt, 0.3);
|
|
CHECK(r.evidence == Evidence::Bright && r.cause == Cause::IrLight);
|
|
CHECK(a.evaluate(250, Source::Ir, true, std::nullopt, 0.14).evidence == Evidence::Neutral);
|
|
// With a hold configured, it must last that long.
|
|
SensorConfig held;
|
|
held.ir_light_hold_s = 0.25;
|
|
EvidenceBuilder b(held);
|
|
CHECK(b.evaluate(0, Source::Ir, true, std::nullopt, 0.3).evidence == Evidence::Neutral);
|
|
r = b.evaluate(250, Source::Ir, true, std::nullopt, 0.3);
|
|
CHECK(r.evidence == Evidence::Bright && r.cause == Cause::IrLight);
|
|
// Dark (0 with the emitters on, 70 s in room B): never.
|
|
EvidenceBuilder c;
|
|
for (std::uint64_t t = 0; t <= 70000; t += 500)
|
|
CHECK(c.evaluate(t, Source::Ir, true, std::nullopt, 0.0).evidence != Evidence::Bright);
|
|
// A dark reading resets the hold.
|
|
EvidenceBuilder d(held);
|
|
d.evaluate(0, Source::Ir, true, std::nullopt, 0.3);
|
|
d.evaluate(125, Source::Ir, true, std::nullopt, 0.0);
|
|
CHECK(d.evaluate(250, Source::Ir, true, std::nullopt, 0.3).evidence != Evidence::Bright);
|
|
// If it misled us (colour dark right after), it is locked out for a while.
|
|
EvidenceBuilder e;
|
|
e.on_switched(0, Source::Color, true, Cause::IrLight);
|
|
CHECK(e.evaluate(300, Source::Color, true, 0.0).cause == Cause::VerifyFailed);
|
|
e.on_switched(600, Source::Ir, true, Cause::VerifyFailed);
|
|
for (std::uint64_t t = 1000; t < 60000; t += 250)
|
|
CHECK(e.evaluate(t, Source::Ir, true, std::nullopt, 0.3).evidence != Evidence::Bright);
|
|
e.evaluate(61000, Source::Ir, true, std::nullopt, 0.3);
|
|
CHECK(e.evaluate(61250, Source::Ir, true, std::nullopt, 0.3).evidence == Evidence::Bright);
|
|
}
|
|
|
|
void test_ir_light_lockout_escalates() {
|
|
// The IR light value misled us (assumed: a surface at medium distance,
|
|
// lit by the emitters, in the dark): after each failed colour check it
|
|
// is ignored for 60 s, then 120 s, ...
|
|
EvidenceBuilder b;
|
|
b.on_switched(10000, Source::Color, true, Cause::IrLight);
|
|
CHECK(b.evaluate(10300, Source::Color, true, 0.0).cause == Cause::VerifyFailed);
|
|
b.on_switched(10600, Source::Ir, true, Cause::VerifyFailed);
|
|
CHECK(!b.wants_ir_light(11000, true));
|
|
for (std::uint64_t t = 11000; t < 70000; t += 500)
|
|
CHECK(b.evaluate(t, Source::Ir, true, std::nullopt, 0.2).evidence != Evidence::Bright);
|
|
CHECK(b.wants_ir_light(70400, true));
|
|
b.on_switched(80000, Source::Color, true, Cause::IrLight);
|
|
b.evaluate(80300, Source::Color, true, 0.0);
|
|
b.on_switched(80600, Source::Ir, true, Cause::VerifyFailed);
|
|
CHECK(!b.wants_ir_light(80300 + 119000, true));
|
|
CHECK(b.wants_ir_light(80300 + 121000, true));
|
|
// Emitters off never needs it unless "off" is distrusted.
|
|
EvidenceBuilder c;
|
|
CHECK(!c.wants_ir_light(0, false));
|
|
CHECK(c.wants_ir_light(0, true));
|
|
}
|
|
|
|
void test_verify_and_distrust() {
|
|
// "Emitters off" took us to colour, but they are back on and colour is
|
|
// dark (a wall fooled XRService): back to IR at once, and "off" is not
|
|
// believed for 60 s, then 120 s, ...
|
|
EvidenceBuilder b;
|
|
b.on_switched(10000, Source::Color, true, Cause::EmittersOff);
|
|
auto r = b.evaluate(10300, Source::Color, true, 0.0);
|
|
CHECK(r.evidence == Evidence::Dark && r.cause == Cause::VerifyFailed && r.urgent);
|
|
CHECK(b.distrusting_emitters_off(10300));
|
|
b.on_switched(10800, Source::Ir, true, Cause::VerifyFailed);
|
|
CHECK(b.evaluate(11000, Source::Ir, false, std::nullopt).evidence != Evidence::Bright);
|
|
CHECK(b.evaluate(70000, Source::Ir, false, std::nullopt).evidence != Evidence::Bright);
|
|
CHECK(b.wants_ir_light(70000, false)); // the IR light value stands in meanwhile
|
|
// It expires by itself: never stuck in IR while the emitters stay off.
|
|
CHECK(!b.distrusting_emitters_off(70400));
|
|
r = b.evaluate(70400, Source::Ir, false, std::nullopt);
|
|
CHECK(r.evidence == Evidence::Bright && r.cause == Cause::EmittersOff);
|
|
// A second mistake doubles it.
|
|
b.on_switched(80000, Source::Color, true, Cause::EmittersOff);
|
|
b.evaluate(80300, Source::Color, true, 0.0);
|
|
CHECK(b.distrusting_emitters_off(80300 + 119000));
|
|
CHECK(!b.distrusting_emitters_off(80300 + 120000));
|
|
|
|
// Emitters off and colour dark right after switching (dusk): fine, no revert.
|
|
EvidenceBuilder f;
|
|
f.on_switched(0, Source::Color, true, Cause::EmittersOff);
|
|
r = f.evaluate(300, Source::Color, false, 0.01);
|
|
CHECK(r.evidence == Evidence::Neutral && !r.urgent && !f.distrusting_emitters_off(300));
|
|
// A correct switch to colour (it is light) passes the check quietly,
|
|
// also while the emitters lag behind.
|
|
EvidenceBuilder c;
|
|
c.on_switched(0, Source::Color, true, Cause::IrLight);
|
|
r = c.evaluate(300, Source::Color, true, 0.9);
|
|
CHECK(r.evidence == Evidence::Neutral && !r.urgent);
|
|
// After the verify window, emitters on and dark colour take the normal
|
|
// (not urgent) route.
|
|
EvidenceBuilder d;
|
|
d.on_switched(0, Source::Color, true, Cause::EmittersOff);
|
|
d.evaluate(3500, Source::Color, true, 0.9);
|
|
r = d.evaluate(4000, Source::Color, true, 0.0);
|
|
CHECK(r.evidence == Evidence::Dark && r.cause == Cause::EmittersOnDim && !r.urgent);
|
|
// A manual switch to colour is not verified.
|
|
EvidenceBuilder e;
|
|
e.on_switched(0, Source::Color, false, Cause::None);
|
|
CHECK(!e.evaluate(300, Source::Color, true, 0.0).urgent);
|
|
}
|
|
|
|
// End to end: a simulated room, XRService's emitters as measured (on ~1 s
|
|
// after darkness, off ~5 s after light returns, fooled off after ~3 s with
|
|
// a wall close to the headset, off at dusk), the colour camera, sensors,
|
|
// policy.
|
|
enum class Wall { None, Close, Medium };
|
|
|
|
struct World {
|
|
bool emitters = false;
|
|
bool sticky = false; // XRService keeps its emitters on once on (00:28-00:30)
|
|
std::uint64_t dark_since = 0, bright_since = 0, wall_since = 0;
|
|
void step(std::uint64_t t, double light, Wall wall_kind) {
|
|
const bool wall = wall_kind == Wall::Close;
|
|
if (wall) {
|
|
if (!wall_since) wall_since = t ? t : 1;
|
|
if (t - wall_since >= 3000) emitters = false; // reflected IR fools XRService
|
|
return;
|
|
}
|
|
wall_since = 0;
|
|
if (light < 0.3 && !dusk(light)) {
|
|
if (!dark_since) dark_since = t ? t : 1;
|
|
bright_since = 0;
|
|
if (t - dark_since >= 1000) emitters = true;
|
|
} else if (light >= 0.6 || dusk(light)) {
|
|
if (!bright_since) bright_since = t ? t : 1;
|
|
dark_since = 0;
|
|
if (t - bright_since >= 5000 && !sticky) emitters = false;
|
|
} else {
|
|
dark_since = bright_since = 0;
|
|
}
|
|
}
|
|
// Dusk (light 0.1..0.3): the colour camera reads ~0 like a dark room,
|
|
// but XRService's cameras manage without emitters.
|
|
static bool dusk(double light) { return light >= 0.1 && light < 0.3; }
|
|
static double colour(double light) { return light >= 0.6 ? 0.9 : light >= 0.3 ? 0.435 : 0.0; }
|
|
// The IR camera's light value (FINDINGS.md 41-42): 0 dark, ~0.3 lit,
|
|
// 0.10 at a close wall lit by the emitters (measured). A surface at
|
|
// medium distance is assumed to fool it (not measured).
|
|
static double ir_light(double light, Wall wall) {
|
|
if (wall == Wall::Close) return 0.10;
|
|
if (wall == Wall::Medium) return 0.2;
|
|
return light >= 0.6 ? 0.3 : 0.0;
|
|
}
|
|
};
|
|
|
|
struct Replay {
|
|
std::vector<std::uint64_t> switches;
|
|
std::vector<Source> to;
|
|
std::uint64_t colour_in_dark_ms = 0; // time spent showing colour while it was dark
|
|
};
|
|
|
|
Replay replay(double (*light)(std::uint64_t), Wall (*wall)(std::uint64_t), std::uint64_t end_ms,
|
|
bool emitters_stuck_off = false, bool sticky = false) {
|
|
LightPolicy p({}, Source::Color);
|
|
EvidenceBuilder b;
|
|
World w;
|
|
w.sticky = sticky;
|
|
Replay out;
|
|
for (std::uint64_t t = 0; t <= end_ms; t += 250) {
|
|
const double l = light(t);
|
|
const Wall wk = wall(t);
|
|
w.step(t, l, wk);
|
|
if (emitters_stuck_off) w.emitters = false;
|
|
const bool colour_shown = p.source() == Source::Color;
|
|
if (colour_shown && l < 0.1) out.colour_in_dark_ms += 250;
|
|
// autopassd reads the IR light value at 2 Hz, only when it could matter.
|
|
std::optional<double> ir;
|
|
if (!colour_shown && b.wants_ir_light(t, w.emitters) && t % 500 == 0) ir = World::ir_light(l, wk);
|
|
const auto r = b.evaluate(t, p.source(), w.emitters,
|
|
colour_shown ? std::optional<double>(World::colour(l)) : std::nullopt, ir);
|
|
const auto d = p.update(t, r.evidence, r.why, r.urgent);
|
|
if (d.changed) {
|
|
out.switches.push_back(t);
|
|
out.to.push_back(d.source);
|
|
b.on_switched(t, d.source, true, r.cause);
|
|
}
|
|
}
|
|
return out;
|
|
}
|
|
|
|
Wall no_wall(std::uint64_t) { return Wall::None; }
|
|
|
|
void print(const char* what, const Replay& r) {
|
|
std::printf("%s: switches at", what);
|
|
for (std::size_t i = 0; i < r.switches.size(); ++i)
|
|
std::printf(" %.2f(%s)", r.switches[i] / 1000.0, r.to[i] == Source::Color ? "colour" : "ir");
|
|
std::printf("; colour shown in the dark %.2f s\n", r.colour_in_dark_ms / 1000.0);
|
|
}
|
|
|
|
void test_end_to_end() {
|
|
// Lights off at 10 s, on at 40 s: IR within ~2 s; colour within ~1 s
|
|
// (the IR light value, held 0.25 s), before XRService's emitters go off.
|
|
auto r = replay([](std::uint64_t t) { return t >= 10000 && t < 40000 ? 0.0 : 1.0; }, no_wall, 80000);
|
|
print("off/on", r);
|
|
CHECK(r.switches.size() == 2);
|
|
if (r.switches.size() == 2) {
|
|
CHECK(r.switches[0] >= 11000 && r.switches[0] <= 12000);
|
|
CHECK(r.switches[1] >= 40500 && r.switches[1] <= 41750);
|
|
}
|
|
|
|
// The 00:28 walk-through: XRService keeps its emitters on through bright
|
|
// rooms. Colour must still come back.
|
|
r = replay([](std::uint64_t t) { return t >= 10000 && t < 40000 ? 0.0 : 1.0; }, no_wall, 80000, false, true);
|
|
print("bright room, emitters stay on", r);
|
|
CHECK(r.switches.size() == 2);
|
|
if (r.switches.size() == 2) CHECK(r.switches[1] >= 40500 && r.switches[1] <= 41750);
|
|
|
|
// Dark throughout with the emitters on: never back to colour.
|
|
r = replay([](std::uint64_t t) { return t >= 5000 ? 0.0 : 1.0; }, no_wall, 180000);
|
|
CHECK(r.switches.size() == 1);
|
|
|
|
// A hand over the cameras for 1.5 s in a lit room: no switch.
|
|
r = replay([](std::uint64_t t) { return t >= 10000 && t < 11500 ? 0.0 : 1.0; }, no_wall, 40000);
|
|
CHECK(r.switches.empty());
|
|
|
|
// Good light, changing views: never switches.
|
|
r = replay([](std::uint64_t t) { return (t / 7000) % 2 ? 1.0 : 0.7; }, no_wall, 120000);
|
|
CHECK(r.switches.empty());
|
|
|
|
// Dimmed to 45%: nothing changes.
|
|
r = replay([](std::uint64_t) { return 0.45; }, no_wall, 60000);
|
|
CHECK(r.switches.empty());
|
|
|
|
// Dusk (2026-10-01 18:41): the colour camera reads dark, XRService's
|
|
// emitters stay off. Never switches.
|
|
r = replay([](std::uint64_t t) { return t >= 10000 ? 0.2 : 1.0; }, no_wall, 180000);
|
|
print("dusk", r);
|
|
CHECK(r.switches.empty());
|
|
|
|
// Lights off (IR), then back to dusk level: the emitters go off, colour
|
|
// returns and stays (the dark colour reading is not a failed check).
|
|
r = replay([](std::uint64_t t) { return t >= 10000 && t < 40000 ? 0.0 : 0.2; }, no_wall, 180000);
|
|
print("dark then dusk", r);
|
|
CHECK(r.switches.size() == 2 && !r.to.empty() && r.to.back() == Source::Color);
|
|
|
|
// Dark room, face near a wall from 20 s to 40 s. XRService turns its
|
|
// emitters off at ~23 s: colour (dark) until they come back on after
|
|
// the wall, then IR stays. The accepted cost of trusting the emitters.
|
|
r = replay([](std::uint64_t) { return 0.0; },
|
|
[](std::uint64_t t) { return t >= 20000 && t < 40000 ? Wall::Close : Wall::None; }, 120000);
|
|
print("close wall in the dark", r);
|
|
CHECK(!r.to.empty() && r.to.back() == Source::Ir);
|
|
CHECK(r.switches.size() <= 3);
|
|
CHECK(r.colour_in_dark_ms <= 22000); // the initial ~1.5 s, plus the time at the wall
|
|
|
|
// A wall at medium distance in the dark for three minutes: the view
|
|
// is assumed to fool the IR light value. Each mistake is a sub-second
|
|
// colour flash, then it is ignored for 60 s, 120 s, ...: a few flashes.
|
|
r = replay([](std::uint64_t) { return 0.0; },
|
|
[](std::uint64_t t) { return t >= 20000 && t < 200000 ? Wall::Medium : Wall::None; }, 240000);
|
|
print("medium wall in the dark", r);
|
|
CHECK(!r.to.empty() && r.to.back() == Source::Ir);
|
|
CHECK(r.switches.size() <= 7);
|
|
CHECK(r.colour_in_dark_ms <= 5000);
|
|
}
|
|
|
|
} // namespace
|
|
|
|
int main() {
|
|
test_parsing();
|
|
test_ema();
|
|
test_colour_rules();
|
|
test_ir_rules();
|
|
test_ir_light();
|
|
test_ir_light_lockout_escalates();
|
|
test_verify_and_distrust();
|
|
test_end_to_end();
|
|
if (failures) { std::printf("%d check(s) failed\n", failures); return 1; }
|
|
std::printf("all sensor tests passed\n");
|
|
return 0;
|
|
}
|