diff --git a/src/persona/IntentEngine.cpp b/src/persona/IntentEngine.cpp index 17cbc772..4f0d7839 100644 --- a/src/persona/IntentEngine.cpp +++ b/src/persona/IntentEngine.cpp @@ -28,7 +28,7 @@ void IntentEngine::begin() { activeSpeechUntilMs_ = 0; soundOrientUntilMs_ = 0; lastEventAtMs_ = lastUpdateMs_; - demoEnabled_ = true; + demoEnabled_ = STACKCHAN_DEMO_ENABLED_AT_BOOT != 0; reducedMotion_ = false; soundAzimuthNorm_ = 0.0f; lastEventStrength_ = 0.0f; diff --git a/src/persona/IntentEngine.hpp b/src/persona/IntentEngine.hpp index a78429f8..bc00ea7e 100644 --- a/src/persona/IntentEngine.hpp +++ b/src/persona/IntentEngine.hpp @@ -9,6 +9,22 @@ #include "persona/SpeechPlanner.hpp" #include "persona/StateMatrix.hpp" +// Demo mode injects a random mode change and a synthetic event every 2.5-6 s. +// That is useful on a bench when showing the face off, and actively harmful the +// rest of the time: it makes the character look random rather than responsive, +// and because every injected event counts as stimulus the robot can never +// accumulate enough drowsiness to fall asleep. The character design has him +// getting heavy-lidded at fatigue 0.45, yawning at 0.62 and asleep at 0.80 after +// roughly 8.5 idle minutes, and none of that can happen while demo mode runs. +// +// Every native test covering sleep, idle life, or character behaviour calls +// setDemoEnabled(false) first for exactly this reason, so the behaviour under +// test was never the shipped default. Boot with it off; `demo on` over serial +// still turns it on for a bench demonstration. +#ifndef STACKCHAN_DEMO_ENABLED_AT_BOOT +#define STACKCHAN_DEMO_ENABLED_AT_BOOT 0 +#endif + namespace stackchan { class IntentEngine { @@ -74,7 +90,7 @@ class IntentEngine { uint32_t activeSpeechUntilMs_ = 0; uint32_t soundOrientUntilMs_ = 0; uint32_t lastEventAtMs_ = 0; - bool demoEnabled_ = true; + bool demoEnabled_ = STACKCHAN_DEMO_ENABLED_AT_BOOT != 0; bool reducedMotion_ = false; float soundAzimuthNorm_ = 0.0f; float lastEventStrength_ = 0.0f; diff --git a/test/test_native_logic/test_main.cpp b/test/test_native_logic/test_main.cpp index ac2160d7..00a329d2 100644 --- a/test/test_native_logic/test_main.cpp +++ b/test/test_native_logic/test_main.cpp @@ -912,6 +912,7 @@ void test_intent_engine_emits_deduped_speech_cue_on_external_event() { void test_intent_engine_demo_can_be_disabled_and_resumed() { IntentEngine engine; engine.begin(); + engine.setDemoEnabled(true, 0); TEST_ASSERT_TRUE(engine.isDemoEnabled()); engine.setDemoEnabled(false, 0); @@ -1280,6 +1281,32 @@ CharacterMode runUntilAsleep(IntentEngine& engine, uint32_t limitMs, uint32_t* a return mode; } +// Demo mode injects a synthetic event every 2.5-6 s, and every injected event is +// stimulus, so with it on the robot can never accumulate sleep pressure. Shipping +// it enabled meant the documented drowsiness ladder could not run on a real +// robot: one observed device stayed awake for 5.9 days. +void test_intent_engine_boots_with_demo_off_so_the_character_can_sleep() { + IntentEngine engine; + engine.begin(); + TEST_ASSERT_FALSE(engine.isDemoEnabled()); + + // Left alone, nothing injects a mode change. With demo mode on this loop sees + // Think/Attend/React appear within the first few seconds. + for (uint32_t nowMs = 1000; nowMs <= 30000; nowMs += 1000) { + const RobotFrame frame = engine.update(nowMs); + TEST_ASSERT_EQUAL(static_cast(CharacterMode::Idle), static_cast(frame.mode)); + } + + // And drowsiness accumulates instead of being reset by injected stimulus, so + // he reaches sleep the way the character design describes. Every other sleep + // test has to call setDemoEnabled(false) first to get here; this one must not + // need to, because that is now the boot state. + uint32_t asleepAtMs = 0; + runUntilAsleep(engine, 900000u, &asleepAtMs); + TEST_ASSERT_TRUE(engine.isAsleep()); +} + + } // namespace void test_sleep_pressure_builds_only_when_left_alone() { @@ -9418,6 +9445,7 @@ int main() { RUN_TEST(test_robot_frame_carries_character_mode_for_renderer); RUN_TEST(test_robot_frame_carries_speech_cue_for_output_adapters); RUN_TEST(test_intent_engine_emits_deduped_speech_cue_on_external_event); + RUN_TEST(test_intent_engine_boots_with_demo_off_so_the_character_can_sleep); RUN_TEST(test_intent_engine_demo_can_be_disabled_and_resumed); RUN_TEST(test_idle_life_breathing_moves_face_and_body_together); RUN_TEST(test_persona_behavior_codegen_exposes_idle_life_tuning);