-
Notifications
You must be signed in to change notification settings - Fork 0
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) andLOVE(pet gesture).ANGRY,SURPRISED, andCONFUSEDare validEye::Moodvalues and have LED/sound table entries, but nothing inmood_manager.cppcurrently callsnoteEvent()with them — they're only reachable via themood <name>serial override.
| 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.
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.
_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.
-
Happy —
_pollCharging()comparesbattery_chargingeachupdate()call; a false->true edge (not "currently charging") callsnoteEvent(Eye::HAPPY, HAPPY_HOLD_MS). Boot-while-already-plugged-in does not fire this — the first read only seeds the edge detector. -
Love —
notePetGesture(direction, durationMs), called from the carousel/touch task. Any nonzerodirectioncounts as a pet, regardless of stroke direction. It callsnoteEvent(Eye::LOVE, LOVE_HOLD_MS)and separately arms a sustained Purr clip fordurationMs + PET_SOUND_TAIL_MS, restarting the clip if it finishes early (_pollPetSound()). -
Generic override —
noteEvent(Eye::Mood m, unsigned long holdMs)is a static, thread-safe entry point (a volatile flag + payload, published flag-last) thatupdate()polls first, ahead of everything else. Any mood can be shown this way forholdMs, then control reverts to the level-evaluator. Only_pollCharging()andnotePetGesture()currently call it in shipped code.
notePetGesture / noteEvent are safe to call from other FreeRTOS tasks;
update() (called from loop()) is the only consumer.
Both the event path (in update()) and the level path (_transition())
do the same three things on every mood change:
-
Eye::setMood(next)— pins the eye animation. - Look up
_ledEffect(next). If it returns a nonzero id, writeBlackboard.led_effectto it viaDataBroker::instance().update(Topic::LED, ...)._transition()(the level path) writesled_effect = 0when 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).MoodManagernever writesled_mode— that field is the user's own LED Mode control (menu/quick-access/dashboard); the mood engine only drivesled_effect, and only actually matters while LED Mode = Auto. -
_maybePlaySound(next)— best-effort clip via the mood->sound table below. No-op if_audiois null,mood_sounds_enabledis off, or the mood has no clip mapped.
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 |
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)".
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 |
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().