feat: pogoda, poprawki w wyświetlaniu. Clean Architecture
This commit is contained in:
@@ -92,6 +92,11 @@ tasks:
|
||||
&& echo "[config-download] Zapisano do data/config.json (tylko gesty, bez WiFi)" \
|
||||
|| { echo "[config-download] BLAD: nie mozna polaczyc z $IP"; exit 1; }
|
||||
|
||||
test:
|
||||
desc: Uruchom testy jednostkowe (native, na Mac)
|
||||
cmds:
|
||||
- pio test -e native
|
||||
|
||||
default:
|
||||
desc: Pokaz liste dostepnych zadan
|
||||
cmds:
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
#include "BuddyLogic.h"
|
||||
#include <algorithm>
|
||||
|
||||
// Replaces Arduino constrain()
|
||||
template<typename T>
|
||||
static T clamp(T v, T lo, T hi) { return std::max(lo, std::min(hi, v)); }
|
||||
|
||||
void initBuddy(BuddyState &b, uint32_t now)
|
||||
{
|
||||
b = {};
|
||||
b.blinkState = BLINK_OPEN;
|
||||
b.blinkRy = EYE_RY;
|
||||
b.lastEvent = now;
|
||||
b.nextBlink = now + 3000;
|
||||
b.nextLook = now + 2000;
|
||||
b.nextZzz = now + 3000;
|
||||
b.nextMicroTremor = now + 500;
|
||||
}
|
||||
|
||||
void setBuddyMood(BuddyState &b, Mood m, uint32_t now, uint32_t durationMs)
|
||||
{
|
||||
b.mood = m;
|
||||
b.revertAt = (durationMs > 0) ? now + durationMs : 0;
|
||||
b.lastEvent = now;
|
||||
switch (m) {
|
||||
case MOOD_HAPPY:
|
||||
b.pupilTargetDx = 0; b.pupilTargetDy = -2; break;
|
||||
case MOOD_SLEEPY:
|
||||
b.pupilTargetDx = 0; b.pupilTargetDy = 4; break;
|
||||
case MOOD_SURPRISED:
|
||||
b.pupilTargetDx = 0; b.pupilTargetDy = 0; break;
|
||||
case MOOD_SAD:
|
||||
b.pupilTargetDx = 0; b.pupilTargetDy = 4; break;
|
||||
case MOOD_ANGRY:
|
||||
b.pupilTargetDx = 2; b.pupilTargetDy = 2; break;
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
|
||||
void updateBuddyAnim(BuddyState &b, uint32_t now, RngFn rng)
|
||||
{
|
||||
// Timed mood revert
|
||||
if (b.revertAt > 0 && now >= b.revertAt) {
|
||||
b.mood = MOOD_NORMAL;
|
||||
b.revertAt = 0;
|
||||
}
|
||||
// Auto-sleep after 5 min idle
|
||||
if (b.mood == MOOD_NORMAL && now - b.lastEvent > 300000UL)
|
||||
setBuddyMood(b, MOOD_SLEEPY, now, 0);
|
||||
|
||||
// Blink state machine (disabled for SURPRISED, EXCITED, WINKs)
|
||||
if (b.mood != MOOD_SURPRISED && b.mood != MOOD_EXCITED &&
|
||||
b.mood != MOOD_WINK_L && b.mood != MOOD_WINK_R)
|
||||
{
|
||||
switch (b.blinkState) {
|
||||
case BLINK_OPEN:
|
||||
if (now >= b.nextBlink)
|
||||
b.blinkState = BLINK_CLOSING;
|
||||
break;
|
||||
case BLINK_CLOSING:
|
||||
if (b.blinkRy > 3)
|
||||
b.blinkRy -= 4;
|
||||
else {
|
||||
b.blinkRy = 1;
|
||||
b.blinkState = BLINK_CLOSED;
|
||||
b.closedTicks = 0;
|
||||
}
|
||||
break;
|
||||
case BLINK_CLOSED:
|
||||
if (b.closedTicks++ >= 2)
|
||||
b.blinkState = BLINK_OPENING;
|
||||
break;
|
||||
case BLINK_OPENING:
|
||||
b.blinkRy += 4;
|
||||
if (b.blinkRy >= EYE_RY) {
|
||||
b.blinkRy = EYE_RY;
|
||||
b.blinkState = BLINK_OPEN;
|
||||
b.nextBlink = now + (int32_t)((b.mood == MOOD_SLEEPY)
|
||||
? rng(800, 2000) : rng(2500, 6000));
|
||||
}
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
b.blinkRy = EYE_RY;
|
||||
}
|
||||
|
||||
// Saccadic pupil movement — fast (3px/tick) when far (|delta|>=4), slow (1px) when close
|
||||
{
|
||||
int8_t ddx = b.pupilTargetDx - b.pupilDx;
|
||||
int8_t ddy = b.pupilTargetDy - b.pupilDy;
|
||||
int8_t absDdx = ddx < 0 ? (int8_t)-ddx : ddx;
|
||||
int8_t absDdy = ddy < 0 ? (int8_t)-ddy : ddy;
|
||||
if (ddx) b.pupilDx = (int8_t)(b.pupilDx + (ddx > 0 ? 1 : -1) * (absDdx >= 4 ? 3 : 1));
|
||||
if (ddy) b.pupilDy = (int8_t)(b.pupilDy + (ddy > 0 ? 1 : -1) * (absDdy >= 4 ? 3 : 1));
|
||||
}
|
||||
|
||||
// Gaze — random saccades + micro-tremor
|
||||
{
|
||||
bool fixated = (b.pupilDx == b.pupilTargetDx && b.pupilDy == b.pupilTargetDy);
|
||||
|
||||
if (b.mood == MOOD_SLEEPY) {
|
||||
if (fixated && now >= b.nextLook) {
|
||||
b.pupilTargetDx = (int8_t)rng(-3, 4);
|
||||
b.pupilTargetDy = (int8_t)rng(2, 6);
|
||||
b.nextLook = now + (uint32_t)rng(4000, 9000);
|
||||
}
|
||||
} else if (b.mood != MOOD_WINK_L && b.mood != MOOD_WINK_R) {
|
||||
if (fixated && now >= b.nextLook) {
|
||||
b.pupilTargetDx = (int8_t)rng(-7, 8);
|
||||
b.pupilTargetDy = (int8_t)rng(-5, 6);
|
||||
b.nextLook = now + (uint32_t)rng(1500, 4500);
|
||||
}
|
||||
if ((b.mood == MOOD_NORMAL || b.mood == MOOD_HAPPY) &&
|
||||
fixated && now >= b.nextMicroTremor)
|
||||
{
|
||||
b.pupilTargetDx = (int8_t)clamp(
|
||||
(int32_t)b.pupilTargetDx + rng(-1, 2), (int32_t)-7, (int32_t)7);
|
||||
b.pupilTargetDy = (int8_t)clamp(
|
||||
(int32_t)b.pupilTargetDy + rng(-1, 2), (int32_t)-5, (int32_t)5);
|
||||
b.nextMicroTremor = now + (uint32_t)rng(300, 700);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ZZZ bubbles when sleepy
|
||||
if (b.mood == MOOD_SLEEPY && now >= b.nextZzz) {
|
||||
b.zzzPhase = (b.zzzPhase % 3) + 1;
|
||||
b.nextZzz = now + 700;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
#pragma once
|
||||
#include "BuddyTypes.h"
|
||||
|
||||
// Eye radius — used in blink state machine and main.cpp drawing
|
||||
static const uint8_t EYE_RY = 15;
|
||||
|
||||
// Inject RNG so tests can supply a deterministic function
|
||||
using RngFn = int32_t(*)(int32_t lo, int32_t hi);
|
||||
|
||||
void initBuddy(BuddyState &b, uint32_t now);
|
||||
void setBuddyMood(BuddyState &b, Mood m, uint32_t now, uint32_t durationMs = 0);
|
||||
void updateBuddyAnim(BuddyState &b, uint32_t now, RngFn rng);
|
||||
@@ -0,0 +1,40 @@
|
||||
#pragma once
|
||||
#include <stdint.h>
|
||||
|
||||
enum Mood : uint8_t {
|
||||
MOOD_NORMAL = 0,
|
||||
MOOD_HAPPY,
|
||||
MOOD_SLEEPY,
|
||||
MOOD_SURPRISED,
|
||||
MOOD_ANGRY,
|
||||
MOOD_SAD,
|
||||
MOOD_EXCITED,
|
||||
MOOD_WINK_L,
|
||||
MOOD_WINK_R,
|
||||
MOOD_HUNGRY,
|
||||
MOOD_PLAYFUL,
|
||||
MOOD_DIRTY
|
||||
};
|
||||
|
||||
enum BlinkState : uint8_t {
|
||||
BLINK_OPEN,
|
||||
BLINK_CLOSING,
|
||||
BLINK_CLOSED,
|
||||
BLINK_OPENING
|
||||
};
|
||||
|
||||
struct BuddyState {
|
||||
Mood mood;
|
||||
uint32_t revertAt;
|
||||
uint32_t lastEvent;
|
||||
BlinkState blinkState;
|
||||
uint8_t blinkRy;
|
||||
uint8_t closedTicks;
|
||||
uint32_t nextBlink;
|
||||
int8_t pupilDx, pupilDy;
|
||||
int8_t pupilTargetDx, pupilTargetDy;
|
||||
uint32_t nextLook;
|
||||
uint8_t zzzPhase;
|
||||
uint32_t nextZzz;
|
||||
uint32_t nextMicroTremor;
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
{ "name": "BuddyDomain", "version": "1.0.0", "frameworks": "*", "platforms": "*" }
|
||||
@@ -0,0 +1,31 @@
|
||||
#include "GestureConfig.h"
|
||||
|
||||
const char *GNAME[NUM_GESTURES] = {
|
||||
"up", "down", "left", "right", "forward", "backward", "clockwise", "anticlockwise", "wave"};
|
||||
|
||||
const char *GKEY[NUM_GESTURES] = {
|
||||
"u", "d", "l", "r", "f", "b", "cw", "ccw", "w"};
|
||||
|
||||
const char *MOOD_LABELS[] = {
|
||||
"-- bez zmiany --", "happy ^_^", "sleepy zZz", "surprised o_O",
|
||||
"angry >_<", "sad T_T", "excited *_*", "wink L ;)", "wink R (;",
|
||||
"hungry :(", "playful :D", "dirty ..."};
|
||||
|
||||
const char *ACTION_LABELS[] = {
|
||||
"-- brak --", "Data i godzina", "Status WiFi",
|
||||
"Nakarm", "Pobaw sie", "Umyj", "Status tamagotchi"};
|
||||
|
||||
const Mood DEFAULT_MOOD[NUM_GESTURES] = {
|
||||
MOOD_HAPPY, // up
|
||||
MOOD_SAD, // down
|
||||
MOOD_SURPRISED, // left
|
||||
MOOD_SURPRISED, // right
|
||||
MOOD_SLEEPY, // forward
|
||||
MOOD_ANGRY, // backward
|
||||
MOOD_EXCITED, // clockwise
|
||||
MOOD_NORMAL, // anticlockwise
|
||||
MOOD_EXCITED, // wave
|
||||
};
|
||||
|
||||
const uint32_t DEFAULT_MOOD_DUR[NUM_GESTURES] = {
|
||||
2000, 2000, 1500, 1500, 0, 3000, 2000, 0, 2000};
|
||||
@@ -0,0 +1,30 @@
|
||||
#pragma once
|
||||
#include <stdint.h>
|
||||
#include "BuddyTypes.h"
|
||||
|
||||
static const uint8_t NUM_GESTURES = 9;
|
||||
static const uint8_t NUM_ACTIONS = 7;
|
||||
|
||||
enum Action : uint8_t {
|
||||
ACTION_NONE = 0,
|
||||
ACTION_DATETIME = 1,
|
||||
ACTION_WIFI = 2,
|
||||
ACTION_FEED = 3,
|
||||
ACTION_PLAY = 4,
|
||||
ACTION_CLEAN = 5,
|
||||
ACTION_STATUS = 6
|
||||
};
|
||||
|
||||
struct GestureConfig {
|
||||
char url[128];
|
||||
uint8_t mood; // 0 = no change, 1-11 matches Mood enum
|
||||
uint8_t action; // Action enum
|
||||
bool enabled;
|
||||
};
|
||||
|
||||
extern const char *GNAME[NUM_GESTURES];
|
||||
extern const char *GKEY[NUM_GESTURES];
|
||||
extern const char *MOOD_LABELS[];
|
||||
extern const char *ACTION_LABELS[];
|
||||
extern const Mood DEFAULT_MOOD[NUM_GESTURES];
|
||||
extern const uint32_t DEFAULT_MOOD_DUR[NUM_GESTURES];
|
||||
@@ -0,0 +1 @@
|
||||
{ "name": "GestureConfig", "version": "1.0.0", "frameworks": "*", "platforms": "*" }
|
||||
@@ -0,0 +1,57 @@
|
||||
#include "TamaLogic.h"
|
||||
|
||||
void initTama(TamaState &t, uint32_t now)
|
||||
{
|
||||
t.hunger = 10;
|
||||
t.happiness = 80;
|
||||
t.hygiene = 90;
|
||||
t.nextHungerTick = now + 120000UL;
|
||||
t.nextHappyTick = now + 180000UL;
|
||||
t.nextHygieneTick = now + 240000UL;
|
||||
}
|
||||
|
||||
Mood updateTama(TamaState &t, const BuddyState &b, uint32_t now)
|
||||
{
|
||||
// Tick needs over time
|
||||
if (now >= t.nextHungerTick) {
|
||||
t.nextHungerTick = now + 120000UL;
|
||||
if (t.hunger < 100) t.hunger++;
|
||||
}
|
||||
if (now >= t.nextHappyTick) {
|
||||
t.nextHappyTick = now + 180000UL;
|
||||
if (t.happiness > 0) t.happiness--;
|
||||
}
|
||||
if (now >= t.nextHygieneTick) {
|
||||
t.nextHygieneTick = now + 240000UL;
|
||||
if (t.hygiene > 0) t.hygiene--;
|
||||
}
|
||||
|
||||
// Return mood override based on critical needs.
|
||||
// Guard: skip when buddy has a timed mood or is in non-overridable state.
|
||||
if (b.revertAt == 0 && b.mood != MOOD_SLEEPY &&
|
||||
b.mood != MOOD_WINK_L && b.mood != MOOD_WINK_R)
|
||||
{
|
||||
if (t.hunger >= 80) return MOOD_HUNGRY;
|
||||
if (t.hygiene <= 20) return MOOD_DIRTY;
|
||||
if (t.happiness <= 20) return MOOD_PLAYFUL;
|
||||
// Needs satisfied — signal "reset to normal" (caller handles if buddy is in need-mood)
|
||||
}
|
||||
return MOOD_NORMAL;
|
||||
}
|
||||
|
||||
void tamaFeed(TamaState &t)
|
||||
{
|
||||
t.hunger = (t.hunger >= 30) ? t.hunger - 30 : 0;
|
||||
}
|
||||
|
||||
void tamaPlay(TamaState &t)
|
||||
{
|
||||
uint16_t v = (uint16_t)t.happiness + 25;
|
||||
t.happiness = (v > 100) ? 100 : (uint8_t)v;
|
||||
}
|
||||
|
||||
void tamaClean(TamaState &t)
|
||||
{
|
||||
uint16_t v = (uint16_t)t.hygiene + 40;
|
||||
t.hygiene = (v > 100) ? 100 : (uint8_t)v;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
#pragma once
|
||||
#include <stdint.h>
|
||||
#include "BuddyTypes.h"
|
||||
|
||||
struct TamaState {
|
||||
uint8_t hunger; // 0=full → 100=starving
|
||||
uint8_t happiness; // 100=happy → 0=bored
|
||||
uint8_t hygiene; // 100=clean → 0=dirty
|
||||
uint32_t nextHungerTick;
|
||||
uint32_t nextHappyTick;
|
||||
uint32_t nextHygieneTick;
|
||||
};
|
||||
|
||||
void initTama(TamaState &t, uint32_t now);
|
||||
|
||||
// Updates ticks and returns the mood override for buddy.
|
||||
// Returns MOOD_HUNGRY / MOOD_DIRTY / MOOD_PLAYFUL when a need is critical.
|
||||
// Returns MOOD_NORMAL when needs are satisfied or guard blocks (revertAt != 0, sleepy, wink).
|
||||
Mood updateTama(TamaState &t, const BuddyState &b, uint32_t now);
|
||||
|
||||
void tamaFeed(TamaState &t); // hunger -= 30 (floor 0)
|
||||
void tamaPlay(TamaState &t); // happy += 25 (cap 100)
|
||||
void tamaClean(TamaState &t); // hygiene += 40 (cap 100)
|
||||
@@ -0,0 +1 @@
|
||||
{ "name": "TamaLogic", "version": "1.0.0", "frameworks": "*", "platforms": "*" }
|
||||
@@ -0,0 +1,87 @@
|
||||
# ESP32-C6 Desk Buddy — Project Memory
|
||||
|
||||
## Hardware
|
||||
- Board: Seeed XIAO ESP32-C6
|
||||
- Display: SSD1306 128x64 OLED
|
||||
- Gesture sensor: CJMCU-7620 (PAJ7620U2, I2C addr 0x73)
|
||||
|
||||
## I2C — CRITICAL
|
||||
- ESP32-C6 has two I2C buses:
|
||||
- I2C0 (HP, `Wire`): configurable to any GPIO — USE THIS
|
||||
- I2C1 (LP, `Wire1`): hardware-locked to SDA=GPIO6, SCL=GPIO7 — NOT usable on XIAO (pins not exposed)
|
||||
- **Both SSD1306 and PAJ7620 share one bus**: `Wire.begin(22, 23)` — SDA=GPIO22(D4), SCL=GPIO23(D5)
|
||||
- SSD1306=0x3C, PAJ7620=0x73 — different addresses, coexist fine
|
||||
- SW I2C (U8g2 bit-bang) blocks CPU ~40ms/frame — kills WiFi on single-core ESP32-C6. Always use HW I2C.
|
||||
- U8g2 HW I2C constructor: `U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, U8X8_PIN_NONE, 23, 22)`
|
||||
|
||||
## PAJ7620 API
|
||||
- Library: `acrandal/RevEng PAJ7620`
|
||||
- Gesture type: `Gesture` (NOT `Gesture_t`)
|
||||
- begin() takes pointer: `sensor.begin(&Wire)` (NOT reference)
|
||||
- `Wire1` pre-defined in framework — do not redeclare `TwoWire Wire1(1)`
|
||||
- Gesture index: `(int)g - 1` maps GES_UP(1)..GES_WAVE(9) to 0..8
|
||||
|
||||
## HTTP Server — CRITICAL
|
||||
- `WebServer.h` does NOT work reliably on ESP32-C6 with IDF 5.x (pioarduino platform)
|
||||
- Use `AsyncWebServer` from `mathieucarbou/ESPAsyncWebServer@^3.3.12`
|
||||
- No `handleClient()` needed — callback-based, works on interrupts
|
||||
- JSON POST body handler: use the 3-arg `on()` with body callback (4th param)
|
||||
|
||||
## NVS / Preferences
|
||||
- Always open with `prefs.begin("namespace", false)` — `true` (read-only) fails with NOT_FOUND if namespace doesn't exist yet
|
||||
- NVS key max 15 chars — use short prefixes: u/d/l/r/f/b/cw/ccw/w for gestures
|
||||
|
||||
## platformio.ini
|
||||
```ini
|
||||
platform = https://github.com/pioarduino/platform-espressif32/releases/download/stable/platform-espressif32.zip
|
||||
board = seeed_xiao_esp32c6
|
||||
framework = arduino
|
||||
lib_deps =
|
||||
olikraus/U8g2
|
||||
acrandal/RevEng PAJ7620
|
||||
bblanchon/ArduinoJson@^7.2.1
|
||||
mathieucarbou/ESPAsyncWebServer@^3.3.12
|
||||
build_flags =
|
||||
-std=gnu++17
|
||||
board_build.partitions = default # no Zigbee partitions needed
|
||||
```
|
||||
|
||||
## Desk Buddy — Architecture
|
||||
- Moods (0-11): NORMAL, HAPPY, SLEEPY, SURPRISED, ANGRY, SAD, EXCITED, WINK_L, WINK_R, HUNGRY, PLAYFUL, DIRTY
|
||||
- Actions (0-6): NONE, DATETIME, WIFI, FEED, PLAY, CLEAN, STATUS
|
||||
- Eyes: `drawFilledEllipse` + mood-specific shapes; wink = `drawCircle` UPPER arc
|
||||
- Wink: WINK_L closes screen-RIGHT eye (!isLeft = buddy's left), WINK_R closes screen-LEFT
|
||||
- Blink: state machine OPEN→CLOSING→CLOSED→OPENING, 4px/tick
|
||||
- Pupil: smooth drift toward random target every 1.5-4s (NORMAL only)
|
||||
- Sleepy: triggered after 5 min idle (`lastEvent` tracking), ZZZ bubbles
|
||||
- Auto-revert: `revertAt` timestamp, 0 = permanent mood
|
||||
- Display refresh: 50ms (~20fps) — OK with HW I2C
|
||||
|
||||
## Webhook System
|
||||
- Per-gesture config: URL (128 chars) + mood (0=none, 1-6) + enabled
|
||||
- Fired async via FreeRTOS `xTaskCreate` — non-blocking
|
||||
- POST `{"gesture":"wave"}` to configured URL, 3s timeout
|
||||
- Config persisted in NVS, served/edited via HTTP at GET/POST /
|
||||
- JSON API: GET /api/config, POST /api/config
|
||||
|
||||
## Key Files
|
||||
- `src/main.cpp` — hardware + drawing + HTTP server + WiFi + FreeRTOS (~400 lines)
|
||||
- `lib/BuddyDomain/` — BuddyTypes.h, BuddyLogic.h/.cpp (mood, blink, pupil — no Arduino)
|
||||
- `lib/TamaLogic/` — TamaLogic.h/.cpp (hunger/happiness/hygiene ticks — no Arduino)
|
||||
- `lib/GestureConfig/` — GestureConfig.h/.cpp (struct, enums, string tables)
|
||||
- `platformio.ini` — [env:esp32-c6] + [env:native]
|
||||
- `test/native/test_buddy/` + `test/native/test_tama/` — Unity tests (40 total)
|
||||
- Reference project: `../handsensor/` — webhook/config patterns
|
||||
|
||||
## Testing
|
||||
- `pio test -e native` or `task test` — runs 40 unit tests on Mac (no device needed)
|
||||
- `pio run -e esp32-c6` — firmware build
|
||||
- Domain libs have zero Arduino deps: `<algorithm>` only; RngFn injection for determinism
|
||||
|
||||
## Domain API
|
||||
- `initBuddy(BuddyState &b, uint32_t now)` — pass millis()
|
||||
- `setBuddyMood(BuddyState &b, Mood m, uint32_t now, uint32_t durationMs=0)`
|
||||
- `updateBuddyAnim(BuddyState &b, uint32_t now, RngFn rng)` — RngFn = int32_t(*)(lo,hi)
|
||||
- `initTama(TamaState &t, uint32_t now)`
|
||||
- `updateTama(TamaState &t, const BuddyState &b, uint32_t now)` → returns Mood override
|
||||
- `tamaFeed/Play/Clean(TamaState &t)` — direct state mutations
|
||||
@@ -0,0 +1,5 @@
|
||||
# Name, Type, SubType, Offset, Size, Flags
|
||||
nvs, data, nvs, 0x9000, 0x5000,
|
||||
otadata, data, ota, 0xe000, 0x2000,
|
||||
app0, app, ota_0, 0x10000, 0x1C0000,
|
||||
spiffs, data, spiffs, 0x1D0000, 0x230000,
|
||||
|
@@ -14,4 +14,16 @@ lib_deps =
|
||||
build_flags =
|
||||
-std=gnu++17
|
||||
-D FORMAT_LITTLEFS_IF_FAILED
|
||||
board_build.partitions = partitions.csv
|
||||
board_build.filesystem = littlefs
|
||||
test_ignore = native/*
|
||||
|
||||
[env:native]
|
||||
platform = native
|
||||
build_flags =
|
||||
-std=c++17
|
||||
-I lib/BuddyDomain
|
||||
-I lib/TamaLogic
|
||||
-I lib/GestureConfig
|
||||
test_build_src = no
|
||||
test_filter = native/*
|
||||
|
||||
+659
-634
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,220 @@
|
||||
#include <unity.h>
|
||||
#include "BuddyLogic.h"
|
||||
|
||||
static int32_t fixed_rng(int32_t lo, int32_t hi) { (void)hi; return lo; }
|
||||
|
||||
void setUp() {}
|
||||
void tearDown() {}
|
||||
|
||||
// ── initBuddy ──────────────────────────────────────────────────────────────
|
||||
|
||||
void test_initBuddy_sets_open_blink() {
|
||||
BuddyState b{};
|
||||
initBuddy(b, 0);
|
||||
TEST_ASSERT_EQUAL(BLINK_OPEN, b.blinkState);
|
||||
TEST_ASSERT_EQUAL(EYE_RY, b.blinkRy);
|
||||
}
|
||||
|
||||
void test_initBuddy_sets_normal_mood() {
|
||||
BuddyState b{};
|
||||
initBuddy(b, 0);
|
||||
TEST_ASSERT_EQUAL(MOOD_NORMAL, b.mood);
|
||||
}
|
||||
|
||||
void test_initBuddy_timestamps_relative_to_now() {
|
||||
BuddyState b{};
|
||||
initBuddy(b, 1000);
|
||||
TEST_ASSERT_EQUAL(1000u, b.lastEvent);
|
||||
TEST_ASSERT_EQUAL(1000u + 3000, b.nextBlink);
|
||||
TEST_ASSERT_EQUAL(1000u + 2000, b.nextLook);
|
||||
}
|
||||
|
||||
// ── setBuddyMood ───────────────────────────────────────────────────────────
|
||||
|
||||
void test_setBuddyMood_no_duration_sets_revertAt_zero() {
|
||||
BuddyState b{};
|
||||
initBuddy(b, 0);
|
||||
setBuddyMood(b, MOOD_HAPPY, 1000, 0);
|
||||
TEST_ASSERT_EQUAL(0u, b.revertAt);
|
||||
}
|
||||
|
||||
void test_setBuddyMood_with_duration_sets_revertAt() {
|
||||
BuddyState b{};
|
||||
initBuddy(b, 0);
|
||||
setBuddyMood(b, MOOD_HAPPY, 1000, 4000);
|
||||
TEST_ASSERT_EQUAL(5000u, b.revertAt);
|
||||
}
|
||||
|
||||
void test_setBuddyMood_updates_lastEvent() {
|
||||
BuddyState b{};
|
||||
initBuddy(b, 0);
|
||||
setBuddyMood(b, MOOD_HAPPY, 5000);
|
||||
TEST_ASSERT_EQUAL(5000u, b.lastEvent);
|
||||
}
|
||||
|
||||
void test_setBuddyMood_happy_sets_pupil_target_up() {
|
||||
BuddyState b{};
|
||||
initBuddy(b, 0);
|
||||
setBuddyMood(b, MOOD_HAPPY, 0);
|
||||
TEST_ASSERT_EQUAL( 0, b.pupilTargetDx);
|
||||
TEST_ASSERT_EQUAL(-2, b.pupilTargetDy);
|
||||
}
|
||||
|
||||
void test_setBuddyMood_sad_sets_pupil_target_down() {
|
||||
BuddyState b{};
|
||||
initBuddy(b, 0);
|
||||
setBuddyMood(b, MOOD_SAD, 0);
|
||||
TEST_ASSERT_EQUAL(0, b.pupilTargetDx);
|
||||
TEST_ASSERT_EQUAL(4, b.pupilTargetDy);
|
||||
}
|
||||
|
||||
// ── updateBuddyAnim — mood revert ─────────────────────────────────────────
|
||||
|
||||
void test_updateBuddyAnim_reverts_mood_when_revertAt_passed() {
|
||||
BuddyState b{};
|
||||
initBuddy(b, 0);
|
||||
setBuddyMood(b, MOOD_HAPPY, 0, 4000); // revertAt = 4000
|
||||
updateBuddyAnim(b, 5000, fixed_rng); // now > revertAt
|
||||
TEST_ASSERT_EQUAL(MOOD_NORMAL, b.mood);
|
||||
TEST_ASSERT_EQUAL(0u, b.revertAt);
|
||||
}
|
||||
|
||||
void test_updateBuddyAnim_does_not_revert_before_revertAt() {
|
||||
BuddyState b{};
|
||||
initBuddy(b, 0);
|
||||
setBuddyMood(b, MOOD_HAPPY, 0, 4000);
|
||||
updateBuddyAnim(b, 3999, fixed_rng);
|
||||
TEST_ASSERT_EQUAL(MOOD_HAPPY, b.mood);
|
||||
}
|
||||
|
||||
void test_updateBuddyAnim_goes_sleepy_after_idle() {
|
||||
BuddyState b{};
|
||||
initBuddy(b, 0); // lastEvent = 0, mood = NORMAL
|
||||
updateBuddyAnim(b, 300001, fixed_rng);
|
||||
TEST_ASSERT_EQUAL(MOOD_SLEEPY, b.mood);
|
||||
}
|
||||
|
||||
void test_updateBuddyAnim_no_sleepy_before_idle() {
|
||||
BuddyState b{};
|
||||
initBuddy(b, 0);
|
||||
updateBuddyAnim(b, 299999, fixed_rng);
|
||||
TEST_ASSERT_EQUAL(MOOD_NORMAL, b.mood);
|
||||
}
|
||||
|
||||
// ── Blink state machine ────────────────────────────────────────────────────
|
||||
|
||||
void test_blink_open_to_closing_when_nextBlink_passed() {
|
||||
BuddyState b{};
|
||||
initBuddy(b, 0); // nextBlink = 3000
|
||||
updateBuddyAnim(b, 3001, fixed_rng);
|
||||
TEST_ASSERT_EQUAL(BLINK_CLOSING, b.blinkState);
|
||||
}
|
||||
|
||||
void test_blink_closing_decrements_blinkRy() {
|
||||
BuddyState b{};
|
||||
initBuddy(b, 0);
|
||||
b.blinkState = BLINK_CLOSING;
|
||||
b.blinkRy = 15;
|
||||
updateBuddyAnim(b, 0, fixed_rng);
|
||||
TEST_ASSERT_EQUAL(11, b.blinkRy);
|
||||
TEST_ASSERT_EQUAL(BLINK_CLOSING, b.blinkState);
|
||||
}
|
||||
|
||||
void test_blink_closing_reaches_closed_when_small() {
|
||||
BuddyState b{};
|
||||
initBuddy(b, 0);
|
||||
b.blinkState = BLINK_CLOSING;
|
||||
b.blinkRy = 3;
|
||||
updateBuddyAnim(b, 0, fixed_rng);
|
||||
TEST_ASSERT_EQUAL(BLINK_CLOSED, b.blinkState);
|
||||
TEST_ASSERT_EQUAL(1, b.blinkRy);
|
||||
}
|
||||
|
||||
void test_blink_closed_ticks_to_opening() {
|
||||
BuddyState b{};
|
||||
initBuddy(b, 0);
|
||||
b.blinkState = BLINK_CLOSED;
|
||||
b.closedTicks = 2;
|
||||
updateBuddyAnim(b, 0, fixed_rng);
|
||||
TEST_ASSERT_EQUAL(BLINK_OPENING, b.blinkState);
|
||||
}
|
||||
|
||||
void test_blink_opening_increments_blinkRy() {
|
||||
BuddyState b{};
|
||||
initBuddy(b, 0);
|
||||
b.blinkState = BLINK_OPENING;
|
||||
b.blinkRy = 7; // 7+4=11 < EYE_RY(15), stays OPENING
|
||||
updateBuddyAnim(b, 0, fixed_rng);
|
||||
TEST_ASSERT_EQUAL(11, b.blinkRy);
|
||||
TEST_ASSERT_EQUAL(BLINK_OPENING, b.blinkState);
|
||||
}
|
||||
|
||||
void test_blink_opening_returns_to_open_at_eye_ry() {
|
||||
BuddyState b{};
|
||||
initBuddy(b, 0);
|
||||
b.blinkState = BLINK_OPENING;
|
||||
b.blinkRy = 13; // 13+4=17 >= EYE_RY(15) → OPEN
|
||||
updateBuddyAnim(b, 0, fixed_rng);
|
||||
TEST_ASSERT_EQUAL(BLINK_OPEN, b.blinkState);
|
||||
TEST_ASSERT_EQUAL(EYE_RY, b.blinkRy);
|
||||
}
|
||||
|
||||
void test_surprised_does_not_blink() {
|
||||
BuddyState b{};
|
||||
initBuddy(b, 0);
|
||||
setBuddyMood(b, MOOD_SURPRISED, 0);
|
||||
b.nextBlink = 0; // past-due
|
||||
updateBuddyAnim(b, 1000, fixed_rng);
|
||||
TEST_ASSERT_EQUAL(BLINK_OPEN, b.blinkState); // no transition
|
||||
TEST_ASSERT_EQUAL(EYE_RY, b.blinkRy); // stays full
|
||||
}
|
||||
|
||||
// ── Pupil saccade ──────────────────────────────────────────────────────────
|
||||
|
||||
void test_pupil_moves_fast_when_far() {
|
||||
BuddyState b{};
|
||||
initBuddy(b, 0);
|
||||
b.pupilDx = 0;
|
||||
b.pupilTargetDx = 8; // |delta| = 8 >= 4 → step 3
|
||||
b.pupilDy = b.pupilTargetDy = 0;
|
||||
updateBuddyAnim(b, 0, fixed_rng);
|
||||
TEST_ASSERT_EQUAL(3, b.pupilDx);
|
||||
}
|
||||
|
||||
void test_pupil_moves_slow_when_close() {
|
||||
BuddyState b{};
|
||||
initBuddy(b, 0);
|
||||
b.pupilDx = 0;
|
||||
b.pupilTargetDx = 2; // |delta| = 2 < 4 → step 1
|
||||
b.pupilDy = b.pupilTargetDy = 0;
|
||||
updateBuddyAnim(b, 0, fixed_rng);
|
||||
TEST_ASSERT_EQUAL(1, b.pupilDx);
|
||||
}
|
||||
|
||||
// ── main ──────────────────────────────────────────────────────────────────
|
||||
|
||||
int main() {
|
||||
UNITY_BEGIN();
|
||||
RUN_TEST(test_initBuddy_sets_open_blink);
|
||||
RUN_TEST(test_initBuddy_sets_normal_mood);
|
||||
RUN_TEST(test_initBuddy_timestamps_relative_to_now);
|
||||
RUN_TEST(test_setBuddyMood_no_duration_sets_revertAt_zero);
|
||||
RUN_TEST(test_setBuddyMood_with_duration_sets_revertAt);
|
||||
RUN_TEST(test_setBuddyMood_updates_lastEvent);
|
||||
RUN_TEST(test_setBuddyMood_happy_sets_pupil_target_up);
|
||||
RUN_TEST(test_setBuddyMood_sad_sets_pupil_target_down);
|
||||
RUN_TEST(test_updateBuddyAnim_reverts_mood_when_revertAt_passed);
|
||||
RUN_TEST(test_updateBuddyAnim_does_not_revert_before_revertAt);
|
||||
RUN_TEST(test_updateBuddyAnim_goes_sleepy_after_idle);
|
||||
RUN_TEST(test_updateBuddyAnim_no_sleepy_before_idle);
|
||||
RUN_TEST(test_blink_open_to_closing_when_nextBlink_passed);
|
||||
RUN_TEST(test_blink_closing_decrements_blinkRy);
|
||||
RUN_TEST(test_blink_closing_reaches_closed_when_small);
|
||||
RUN_TEST(test_blink_closed_ticks_to_opening);
|
||||
RUN_TEST(test_blink_opening_increments_blinkRy);
|
||||
RUN_TEST(test_blink_opening_returns_to_open_at_eye_ry);
|
||||
RUN_TEST(test_surprised_does_not_blink);
|
||||
RUN_TEST(test_pupil_moves_fast_when_far);
|
||||
RUN_TEST(test_pupil_moves_slow_when_close);
|
||||
return UNITY_END();
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
#include <unity.h>
|
||||
#include "TamaLogic.h"
|
||||
|
||||
void setUp() {}
|
||||
void tearDown() {}
|
||||
|
||||
// Helper: build a minimal BuddyState for updateTama calls
|
||||
static BuddyState makeBuddy(uint32_t revertAt = 0, Mood m = MOOD_NORMAL) {
|
||||
BuddyState b{};
|
||||
b.revertAt = revertAt;
|
||||
b.mood = m;
|
||||
return b;
|
||||
}
|
||||
|
||||
// ── initTama ──────────────────────────────────────────────────────────────
|
||||
|
||||
void test_initTama_sets_initial_values() {
|
||||
TamaState t{};
|
||||
initTama(t, 0);
|
||||
TEST_ASSERT_EQUAL(10, t.hunger);
|
||||
TEST_ASSERT_EQUAL(80, t.happiness);
|
||||
TEST_ASSERT_EQUAL(90, t.hygiene);
|
||||
}
|
||||
|
||||
void test_initTama_sets_tick_timestamps() {
|
||||
TamaState t{};
|
||||
initTama(t, 1000);
|
||||
TEST_ASSERT_EQUAL(1000u + 120000u, t.nextHungerTick);
|
||||
TEST_ASSERT_EQUAL(1000u + 180000u, t.nextHappyTick);
|
||||
TEST_ASSERT_EQUAL(1000u + 240000u, t.nextHygieneTick);
|
||||
}
|
||||
|
||||
// ── updateTama ticks ──────────────────────────────────────────────────────
|
||||
|
||||
void test_updateTama_increments_hunger_on_tick() {
|
||||
TamaState t{};
|
||||
initTama(t, 0); // nextHungerTick = 120000
|
||||
BuddyState b = makeBuddy();
|
||||
updateTama(t, b, 120000);
|
||||
TEST_ASSERT_EQUAL(11, t.hunger);
|
||||
}
|
||||
|
||||
void test_updateTama_does_not_increment_hunger_before_tick() {
|
||||
TamaState t{};
|
||||
initTama(t, 0);
|
||||
BuddyState b = makeBuddy();
|
||||
updateTama(t, b, 119999);
|
||||
TEST_ASSERT_EQUAL(10, t.hunger);
|
||||
}
|
||||
|
||||
void test_updateTama_caps_hunger_at_100() {
|
||||
TamaState t{};
|
||||
initTama(t, 0);
|
||||
t.hunger = 100;
|
||||
BuddyState b = makeBuddy();
|
||||
updateTama(t, b, 120000);
|
||||
TEST_ASSERT_EQUAL(100, t.hunger);
|
||||
}
|
||||
|
||||
void test_updateTama_decrements_happiness_on_tick() {
|
||||
TamaState t{};
|
||||
initTama(t, 0); // nextHappyTick = 180000
|
||||
BuddyState b = makeBuddy();
|
||||
updateTama(t, b, 180000);
|
||||
TEST_ASSERT_EQUAL(79, t.happiness);
|
||||
}
|
||||
|
||||
void test_updateTama_caps_happiness_at_zero() {
|
||||
TamaState t{};
|
||||
initTama(t, 0);
|
||||
t.happiness = 0;
|
||||
BuddyState b = makeBuddy();
|
||||
updateTama(t, b, 180000);
|
||||
TEST_ASSERT_EQUAL(0, t.happiness);
|
||||
}
|
||||
|
||||
void test_updateTama_decrements_hygiene_on_tick() {
|
||||
TamaState t{};
|
||||
initTama(t, 0); // nextHygieneTick = 240000
|
||||
BuddyState b = makeBuddy();
|
||||
updateTama(t, b, 240000);
|
||||
TEST_ASSERT_EQUAL(89, t.hygiene);
|
||||
}
|
||||
|
||||
// ── updateTama mood returns ───────────────────────────────────────────────
|
||||
|
||||
void test_updateTama_returns_hungry_when_hunger_high() {
|
||||
TamaState t{};
|
||||
initTama(t, 0);
|
||||
t.hunger = 80;
|
||||
t.nextHungerTick = 999999; // prevent tick
|
||||
BuddyState b = makeBuddy();
|
||||
TEST_ASSERT_EQUAL(MOOD_HUNGRY, updateTama(t, b, 0));
|
||||
}
|
||||
|
||||
void test_updateTama_returns_dirty_when_hygiene_low() {
|
||||
TamaState t{};
|
||||
initTama(t, 0);
|
||||
t.hygiene = 20;
|
||||
t.nextHygieneTick = 999999;
|
||||
BuddyState b = makeBuddy();
|
||||
TEST_ASSERT_EQUAL(MOOD_DIRTY, updateTama(t, b, 0));
|
||||
}
|
||||
|
||||
void test_updateTama_returns_playful_when_happiness_low() {
|
||||
TamaState t{};
|
||||
initTama(t, 0);
|
||||
t.happiness = 20;
|
||||
t.nextHappyTick = 999999;
|
||||
BuddyState b = makeBuddy();
|
||||
TEST_ASSERT_EQUAL(MOOD_PLAYFUL, updateTama(t, b, 0));
|
||||
}
|
||||
|
||||
void test_updateTama_no_override_when_revertAt_set() {
|
||||
TamaState t{};
|
||||
initTama(t, 0);
|
||||
t.hunger = 90;
|
||||
t.nextHungerTick = 999999;
|
||||
BuddyState b = makeBuddy(5000); // revertAt != 0 → guard blocks
|
||||
TEST_ASSERT_EQUAL(MOOD_NORMAL, updateTama(t, b, 0));
|
||||
}
|
||||
|
||||
void test_updateTama_returns_normal_when_needs_ok() {
|
||||
TamaState t{};
|
||||
initTama(t, 0); // hunger=10, happy=80, hygiene=90 — all fine
|
||||
t.nextHungerTick = t.nextHappyTick = t.nextHygieneTick = 999999;
|
||||
BuddyState b = makeBuddy();
|
||||
TEST_ASSERT_EQUAL(MOOD_NORMAL, updateTama(t, b, 0));
|
||||
}
|
||||
|
||||
// ── tamaFeed ──────────────────────────────────────────────────────────────
|
||||
|
||||
void test_tamaFeed_reduces_hunger_by_30() {
|
||||
TamaState t{};
|
||||
t.hunger = 50;
|
||||
tamaFeed(t);
|
||||
TEST_ASSERT_EQUAL(20, t.hunger);
|
||||
}
|
||||
|
||||
void test_tamaFeed_floors_hunger_at_zero() {
|
||||
TamaState t{};
|
||||
t.hunger = 10;
|
||||
tamaFeed(t);
|
||||
TEST_ASSERT_EQUAL(0, t.hunger);
|
||||
}
|
||||
|
||||
// ── tamaPlay ──────────────────────────────────────────────────────────────
|
||||
|
||||
void test_tamaPlay_increases_happiness_by_25() {
|
||||
TamaState t{};
|
||||
t.happiness = 50;
|
||||
tamaPlay(t);
|
||||
TEST_ASSERT_EQUAL(75, t.happiness);
|
||||
}
|
||||
|
||||
void test_tamaPlay_caps_happiness_at_100() {
|
||||
TamaState t{};
|
||||
t.happiness = 90;
|
||||
tamaPlay(t);
|
||||
TEST_ASSERT_EQUAL(100, t.happiness);
|
||||
}
|
||||
|
||||
// ── tamaClean ─────────────────────────────────────────────────────────────
|
||||
|
||||
void test_tamaClean_increases_hygiene_by_40() {
|
||||
TamaState t{};
|
||||
t.hygiene = 50;
|
||||
tamaClean(t);
|
||||
TEST_ASSERT_EQUAL(90, t.hygiene);
|
||||
}
|
||||
|
||||
void test_tamaClean_caps_hygiene_at_100() {
|
||||
TamaState t{};
|
||||
t.hygiene = 80;
|
||||
tamaClean(t);
|
||||
TEST_ASSERT_EQUAL(100, t.hygiene);
|
||||
}
|
||||
|
||||
// ── main ──────────────────────────────────────────────────────────────────
|
||||
|
||||
int main() {
|
||||
UNITY_BEGIN();
|
||||
RUN_TEST(test_initTama_sets_initial_values);
|
||||
RUN_TEST(test_initTama_sets_tick_timestamps);
|
||||
RUN_TEST(test_updateTama_increments_hunger_on_tick);
|
||||
RUN_TEST(test_updateTama_does_not_increment_hunger_before_tick);
|
||||
RUN_TEST(test_updateTama_caps_hunger_at_100);
|
||||
RUN_TEST(test_updateTama_decrements_happiness_on_tick);
|
||||
RUN_TEST(test_updateTama_caps_happiness_at_zero);
|
||||
RUN_TEST(test_updateTama_decrements_hygiene_on_tick);
|
||||
RUN_TEST(test_updateTama_returns_hungry_when_hunger_high);
|
||||
RUN_TEST(test_updateTama_returns_dirty_when_hygiene_low);
|
||||
RUN_TEST(test_updateTama_returns_playful_when_happiness_low);
|
||||
RUN_TEST(test_updateTama_no_override_when_revertAt_set);
|
||||
RUN_TEST(test_updateTama_returns_normal_when_needs_ok);
|
||||
RUN_TEST(test_tamaFeed_reduces_hunger_by_30);
|
||||
RUN_TEST(test_tamaFeed_floors_hunger_at_zero);
|
||||
RUN_TEST(test_tamaPlay_increases_happiness_by_25);
|
||||
RUN_TEST(test_tamaPlay_caps_happiness_at_100);
|
||||
RUN_TEST(test_tamaClean_increases_hygiene_by_40);
|
||||
RUN_TEST(test_tamaClean_caps_hygiene_at_100);
|
||||
return UNITY_END();
|
||||
}
|
||||
Reference in New Issue
Block a user