Skip to content

mood manager

Wouter Van de Wiele edited this page Aug 5, 2026 · 1 revision

Mood Manager

lib/MOOD_MANAGER/ turns real sensor input into a "personality": it selects an eye-animation mood (Eye::Mood), optionally drives a matching LED effect via the Blackboard, and — when an AudioManager is wired up — plays a matching cat sound.

There are 12 moods total (Eye::Mood, lib/GUI_Eye/Eye.h): NEUTRAL, HAPPY, SAD, EXCITED, EVIL, ANGRY, SLEEPY, SURPRISED, CONFUSED, NERVOUS, LOVE, BORED. Every mood falls into one of two categories:

  • Level moods — driven continuously by _evaluateLevel() from sensor state: NEUTRAL, SLEEPY, EVIL, BORED, NERVOUS, SAD, EXCITED.
  • Event moods — fired once by an external trigger via noteEvent() / notePetGesture(), shown for a fixed hold time, then handed back to the level-evaluator: HAPPY (charger plug-in edge) and LOVE (pet gesture). ANGRY, SURPRISED, and CONFUSED are valid Eye::Mood values and have LED/sound table entries, but nothing in mood_manager.cpp currently calls noteEvent() with them — they're only reachable via the mood <name> serial override.

Timing model

Constant Value Meaning
HAPPY_HOLD_MS 30 s how long a Happy event mood is shown before control returns to the level-evaluator
LOVE_HOLD_MS 30 s how long a Love event mood is shown before control returns to the level-evaluator
PET_SOUND_TAIL_MS 2 s extra time the sustained pet Purr clip keeps playing past the moment the finger lifts
BORED_IDLE_MS 2 h no button/pet/motion activity for this long -> Bored
BORED_POLL_MS 60 s how often the idle threshold is re-checked (idle time isn't a Blackboard-field edge, so it can't piggyback on the other level moods' change-detection)
BATT_LOW_MV 3400 mV flat battery_adc_1min.avg threshold, no hysteresis band; charging bypasses the check entirely
MOVING_THRESHOLD 0.3 |accel magnitude - 1g| beyond which the robot counts as "moving"

There is no periodic re-evaluation timer and no minimum hold time for level moods — _evaluateLevel() runs every update() call but only acts when one of its four tracked inputs (dark/battLow/wifi/moving) actually changes, or every BORED_POLL_MS to re-check idle time.

Level-evaluator inputs

Read from DataBroker::instance().snapshot() each update() call:

Signal Derivation
dark snap.ambient_dark (LDR EMA + hysteresis, computed elsewhere)
battLow !battery_charging && battery_adc_1min.valid && battery_adc_1min.avg < BATT_LOW_MV
wifi snap.wifi_connected
moving fabsf(sqrt(accel_x²+accel_y²+accel_z²) - 1.0f) > MOVING_THRESHOLD
idleMs CarouselManager::instance().msSinceActivity(now)

There is no weather integration, no direct use of battery_voltage or ldr_ohms, and no "isNight"/"shaking"/"petRecent" scoring inputs — those belong to an earlier design, not the current code.

Level priority chain

_evaluateLevel() (mood_manager.cpp:96-145) is a simple if/else-if priority chain, not a scoring system. First match wins:

dark && moving          -> EVIL
dark                     -> SLEEPY
idleMs > BORED_IDLE_MS   -> BORED
battLow && !wifi         -> NERVOUS
battLow && wifi           -> SAD
moving && !battLow        -> EXCITED
else                       -> NEUTRAL

_evaluateLevel() only runs the chain when dark, battLow, wifi, or moving changed since the last check (or every BORED_POLL_MS, or once at boot). Every time it does run, it logs:

[mood] dark=%d battLow=%d wifi=%d moving=%d idle=%lus -> %s

HAPPY and LOVE never appear in this chain — they can only be shown via an event (see below), and the moment the event's hold timer expires, _evaluateLevel() is force-run to pick the current level mood back up.

Event moods

  • Happy_pollCharging() compares battery_charging each update() call; a false->true edge (not "currently charging") calls noteEvent(Eye::HAPPY, HAPPY_HOLD_MS). Boot-while-already-plugged-in does not fire this — the first read only seeds the edge detector.
  • LovenotePetGesture(direction, durationMs), called from the carousel/touch task. Any nonzero direction counts as a pet, regardless of stroke direction. It calls noteEvent(Eye::LOVE, LOVE_HOLD_MS) and separately arms a sustained Purr clip for durationMs + PET_SOUND_TAIL_MS, restarting the clip if it finishes early (_pollPetSound()).
  • Generic overridenoteEvent(Eye::Mood m, unsigned long holdMs) is a static, thread-safe entry point (a volatile flag + payload, published flag-last) that update() polls first, ahead of everything else. Any mood can be shown this way for holdMs, then control reverts to the level-evaluator. Only _pollCharging() and notePetGesture() currently call it in shipped code.

notePetGesture / noteEvent are safe to call from other FreeRTOS tasks; update() (called from loop()) is the only consumer.

Transition side effects

Both the event path (in update()) and the level path (_transition()) do the same three things on every mood change:

  1. Eye::setMood(next) — pins the eye animation.
  2. Look up _ledEffect(next). If it returns a nonzero id, write Blackboard.led_effect to it via DataBroker::instance().update(Topic::LED, ...). _transition() (the level path) writes led_effect = 0 when the mood has no LED entry, releasing any effect the previous mood set; the event path leaves whatever was already showing untouched when the id is 0 (so Happy/ Surprised/Confused don't blank an in-progress level-mood effect). MoodManager never writes led_mode — that field is the user's own LED Mode control (menu/quick-access/dashboard); the mood engine only drives led_effect, and only actually matters while LED Mode = Auto.
  3. _maybePlaySound(next) — best-effort clip via the mood->sound table below. No-op if _audio is null, mood_sounds_enabled is off, or the mood has no clip mapped.

LED effect mapping (_ledEffect(), mood_manager.cpp:172-184)

Effect ids match effectOpts in lib/GUI_Menu/menu_element.cpp.

Mood Effect id Effect name
SAD 1 Breath
LOVE 3 Purr
SLEEPY 5 Sleep
BORED 6 Scan
EXCITED 8 Curious
ANGRY 9 Angry
NERVOUS 13 Thunder
EVIL 14 Hypno
NEUTRAL / HAPPY / SURPRISED / CONFUSED 0 none — no LED reaction, current effect (if any) is released

Sound mapping (_soundForMood(), mood_manager.cpp:192-199)

MoodManager links against lib/AUDIO_MANAGER/audio_manager.h's Sound enum (14 compiled-in mp3 byte arrays — not SPIFFS files): Angry, Bored, Calling, Confused, CoolOpera, CoolWobble, Cute, Hissing, InnerAngry, Purr, Snore, SoftAngry, Trilling, Wink.

Mood Sound Trigger
HAPPY Purr charger plug-in (via _maybePlaySound)
ANGRY Angry via mood angry / noteEvent(ANGRY, ...) if ever called
BORED Bored idle > 2h
LOVE Purr not via this table — sustained separately by _pollPetSound() for the pet-gesture duration + tail
NEUTRAL / SAD / EXCITED / EVIL / SLEEPY / SURPRISED / CONFUSED / NERVOUS no automatic sound

All sound playback is gated by Blackboard.mood_sounds_enabled (NVS key mood_snd_en, toggled by the Mood page's "Audio" menu item) and by _audio being non-null. _audio (an AudioManager*) is only null in BootMode::WEB, where no audio hardware is allocated; it's set and functional in the default BootMode::STREAM boot path. AudioManager::playSound() can also refuse a clip if web radio is currently active — logged as "refused (radio active)".

Menu integration

The GUI Mood page (lib/GUI_Menu/menu_element.cpp) has no separate "Mood LED" toggle. Its items are:

Item Backs Effect
Audio mood_sounds_enabled master gate for mood-triggered clips (Purr on Happy/Love, Angry, Bored)
LED Mode led_mode (Off / Auto / Manual) Auto is what lets MoodManager's led_effect writes actually show on the strip
Effect led_effect manual effect pick, only meaningful in Manual mode
Brightness / Eye Dim led_effect_brightness / led_eye_dim brightness controls, independent of mood logic

Serial debug interface

MoodManager::update() polls the serial port. Commands (prefix mood or m , plus standalone accel/sound):

Command Effect
mood list print all 12 mood names
mood <name> force a mood immediately via _transition() (case-insensitive)
mood status print the current mood name only — no hold time or sound-budget info, there is no such subsystem
accel on / accel off print accel_x/y/z plus the equivalent GameTiltMaze ball input every 100ms
sound <name> play a clip by name via AudioManager::playSound() (unavailable in WEB boot mode)
sound list print all Sound enum names
sound stop stop the currently playing clip

mood auto and mood score do not exist — there is no separate "resume auto" command (the level-evaluator always resumes on its own once an event mood's hold timer expires) and no score table to dump.

Boot banner and full command list are printed once from begin().

Clone this wiki locally