Refactor platformio.ini for ESP32-S3 and add Ambient Light Sensor functionality with adaptive display contrast
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
[env:esp32-c3]
|
[env:esp32-s3]
|
||||||
platform = espressif32
|
platform = espressif32
|
||||||
board = seeed_xiao_esp32c3
|
board = esp32-s3-devkitc-1
|
||||||
framework = arduino
|
framework = arduino
|
||||||
|
|
||||||
monitor_speed = 115200
|
monitor_speed = 115200
|
||||||
@@ -8,6 +8,7 @@ monitor_speed = 115200
|
|||||||
lib_deps =
|
lib_deps =
|
||||||
adafruit/Adafruit GFX Library
|
adafruit/Adafruit GFX Library
|
||||||
adafruit/Adafruit SSD1306
|
adafruit/Adafruit SSD1306
|
||||||
|
adafruit/Adafruit VEML7700 Library
|
||||||
bblanchon/ArduinoJson
|
bblanchon/ArduinoJson
|
||||||
|
|
||||||
; board_build.filesystem = spiffs
|
; board_build.filesystem = spiffs
|
||||||
@@ -16,3 +17,4 @@ lib_deps =
|
|||||||
build_flags =
|
build_flags =
|
||||||
-D PB_VERSION=\"1.0.0\"
|
-D PB_VERSION=\"1.0.0\"
|
||||||
-D PB_HOSTNAME=\"faceplant\"
|
-D PB_HOSTNAME=\"faceplant\"
|
||||||
|
-D PB_TZ=\"UTC0\"
|
||||||
|
|||||||
145
src/app/App.cpp
145
src/app/App.cpp
@@ -12,7 +12,9 @@
|
|||||||
#include "../ui/Display.h"
|
#include "../ui/Display.h"
|
||||||
#include "../sensors/MoistureSensor.h"
|
#include "../sensors/MoistureSensor.h"
|
||||||
#include "../sensors/BatterySensor.h"
|
#include "../sensors/BatterySensor.h"
|
||||||
|
#include "../sensors/AmbientLightSensor.h"
|
||||||
#include "../ui/FaceRenderer.h"
|
#include "../ui/FaceRenderer.h"
|
||||||
|
#include <time.h>
|
||||||
|
|
||||||
static Settings settings;
|
static Settings settings;
|
||||||
static WiFiManager wifi;
|
static WiFiManager wifi;
|
||||||
@@ -23,12 +25,29 @@ static WebhookService webhook;
|
|||||||
static Display display;
|
static Display display;
|
||||||
static MoistureSensor moisture;
|
static MoistureSensor moisture;
|
||||||
static BatterySensor battery;
|
static BatterySensor battery;
|
||||||
|
static AmbientLightSensor ambient;
|
||||||
static FaceRenderer face;
|
static FaceRenderer face;
|
||||||
|
|
||||||
static unsigned long bootMs = 0;
|
static unsigned long bootMs = 0;
|
||||||
|
|
||||||
static FaceRenderer::Mood lastMood = FaceRenderer::HAPPY;
|
static FaceRenderer::Mood lastMood = FaceRenderer::HAPPY;
|
||||||
static bool lastDead = false;
|
static bool lastDead = false;
|
||||||
|
static bool isNightMode = false;
|
||||||
|
static uint8_t lastContrast = 0xFF;
|
||||||
|
static bool ntpConfigured = false;
|
||||||
|
static bool timeValid = false;
|
||||||
|
static unsigned long nextTimeCheckMs = 0;
|
||||||
|
static bool lastScheduleSleep = false;
|
||||||
|
|
||||||
|
static constexpr float DIM_LUX_MIN = 0.0f;
|
||||||
|
static constexpr float DIM_LUX_MAX = 300.0f;
|
||||||
|
static constexpr uint8_t DIM_CONTRAST_MIN = 0x10;
|
||||||
|
static constexpr uint8_t DIM_CONTRAST_MAX = 0xFF;
|
||||||
|
static constexpr int BED_HOUR = 22;
|
||||||
|
static constexpr int BED_MINUTE = 0;
|
||||||
|
static constexpr int WAKE_HOUR = 7;
|
||||||
|
static constexpr int WAKE_MINUTE = 0;
|
||||||
|
static constexpr int ROUTINE_WINDOW_MIN = 5;
|
||||||
|
|
||||||
static PlantEventType moodToEvent(FaceRenderer::Mood m) {
|
static PlantEventType moodToEvent(FaceRenderer::Mood m) {
|
||||||
if (m == FaceRenderer::DRY) return EVT_DRY;
|
if (m == FaceRenderer::DRY) return EVT_DRY;
|
||||||
@@ -36,6 +55,123 @@ static PlantEventType moodToEvent(FaceRenderer::Mood m) {
|
|||||||
return EVT_OK;
|
return EVT_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static float clampFloat(float v, float lo, float hi) {
|
||||||
|
if (v < lo) return lo;
|
||||||
|
if (v > hi) return hi;
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
static uint8_t luxToContrast(float lux) {
|
||||||
|
float clamped = clampFloat(lux, DIM_LUX_MIN, DIM_LUX_MAX);
|
||||||
|
float ratio = (DIM_LUX_MAX <= DIM_LUX_MIN) ? 1.0f
|
||||||
|
: (clamped - DIM_LUX_MIN) / (DIM_LUX_MAX - DIM_LUX_MIN);
|
||||||
|
int value = (int)(DIM_CONTRAST_MIN + ratio * (DIM_CONTRAST_MAX - DIM_CONTRAST_MIN));
|
||||||
|
if (value < 0) value = 0;
|
||||||
|
if (value > 255) value = 255;
|
||||||
|
return (uint8_t)value;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ScheduleState {
|
||||||
|
bool hasTime = false;
|
||||||
|
bool sleeping = false;
|
||||||
|
FaceRenderer::RoutineAnim routineAnim = FaceRenderer::ROUTINE_NONE;
|
||||||
|
uint16_t routineProgressPermille = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
static int secondsOfDay(const tm& local) {
|
||||||
|
return local.tm_hour * 3600 + local.tm_min * 60 + local.tm_sec;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool isInRangeSameDay(int sec, int startSec, int endSec) {
|
||||||
|
return sec >= startSec && sec < endSec;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool isInOvernightRange(int sec, int startSec, int endSec) {
|
||||||
|
return (sec >= startSec) || (sec < endSec);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void maybeInitTimeSync(bool wifiConnected) {
|
||||||
|
if (!wifiConnected || ntpConfigured) return;
|
||||||
|
configTzTime(PB_TZ, "pool.ntp.org", "time.nist.gov");
|
||||||
|
ntpConfigured = true;
|
||||||
|
Serial.print("[Clock] NTP started, TZ=");
|
||||||
|
Serial.println(PB_TZ);
|
||||||
|
}
|
||||||
|
|
||||||
|
static ScheduleState currentScheduleState() {
|
||||||
|
ScheduleState s;
|
||||||
|
time_t nowEpoch = time(nullptr);
|
||||||
|
if (nowEpoch < 1700000000) return s; // not synced yet
|
||||||
|
|
||||||
|
tm local {};
|
||||||
|
if (!localtime_r(&nowEpoch, &local)) return s;
|
||||||
|
s.hasTime = true;
|
||||||
|
|
||||||
|
const int sec = secondsOfDay(local);
|
||||||
|
const int bedSec = BED_HOUR * 3600 + BED_MINUTE * 60;
|
||||||
|
const int wakeSec = WAKE_HOUR * 3600 + WAKE_MINUTE * 60;
|
||||||
|
const int windDownStartSec = bedSec - ROUTINE_WINDOW_MIN * 60;
|
||||||
|
const int wakeAnimEndSec = wakeSec + ROUTINE_WINDOW_MIN * 60;
|
||||||
|
|
||||||
|
if (isInRangeSameDay(sec, windDownStartSec, bedSec)) {
|
||||||
|
s.routineAnim = FaceRenderer::ROUTINE_SLEEPING_SOON;
|
||||||
|
s.routineProgressPermille = (uint16_t)(((sec - windDownStartSec) * 1000L) / (ROUTINE_WINDOW_MIN * 60L));
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isInOvernightRange(sec, bedSec, wakeSec)) {
|
||||||
|
s.sleeping = true;
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isInRangeSameDay(sec, wakeSec, wakeAnimEndSec)) {
|
||||||
|
s.routineAnim = FaceRenderer::ROUTINE_WAKING_UP;
|
||||||
|
s.routineProgressPermille = (uint16_t)(((sec - wakeSec) * 1000L) / (ROUTINE_WINDOW_MIN * 60L));
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void updateScheduleState() {
|
||||||
|
if ((long)(millis() - nextTimeCheckMs) < 0) return;
|
||||||
|
nextTimeCheckMs = millis() + 1000;
|
||||||
|
|
||||||
|
maybeInitTimeSync(wifi.connected());
|
||||||
|
ScheduleState sched = currentScheduleState();
|
||||||
|
timeValid = sched.hasTime;
|
||||||
|
|
||||||
|
face.setRoutineAnimation(sched.routineAnim, sched.routineProgressPermille);
|
||||||
|
|
||||||
|
bool shouldSleep = sched.sleeping;
|
||||||
|
if (!sched.hasTime) shouldSleep = false; // fail-safe: stay awake until time sync exists
|
||||||
|
|
||||||
|
if (shouldSleep != lastScheduleSleep) {
|
||||||
|
Serial.print("[Clock] Schedule ");
|
||||||
|
Serial.println(shouldSleep ? "sleep window entered" : "wake window entered");
|
||||||
|
lastScheduleSleep = shouldSleep;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool wasNightMode = isNightMode;
|
||||||
|
isNightMode = shouldSleep;
|
||||||
|
if (display.ok()) {
|
||||||
|
display.setDisplayEnabled(!isNightMode);
|
||||||
|
if (wasNightMode && !isNightMode) lastContrast = 0xFF; // force contrast refresh when waking
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void updateAmbientDimming() {
|
||||||
|
ambient.loop();
|
||||||
|
if (!ambient.available() || !display.ok() || isNightMode || !display.displayEnabled()) return;
|
||||||
|
|
||||||
|
float lux = ambient.filteredLux();
|
||||||
|
uint8_t contrast = luxToContrast(lux);
|
||||||
|
if (lastContrast == 0xFF || abs((int)contrast - (int)lastContrast) >= 4) {
|
||||||
|
display.setContrast(contrast);
|
||||||
|
lastContrast = contrast;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void App::setup() {
|
void App::setup() {
|
||||||
bootMs = millis();
|
bootMs = millis();
|
||||||
|
|
||||||
@@ -60,6 +196,7 @@ void App::setup() {
|
|||||||
|
|
||||||
moisture.begin(settings);
|
moisture.begin(settings);
|
||||||
battery.begin();
|
battery.begin();
|
||||||
|
ambient.begin();
|
||||||
face.begin(display, settings);
|
face.begin(display, settings);
|
||||||
|
|
||||||
webhook.begin(settings);
|
webhook.begin(settings);
|
||||||
@@ -81,17 +218,21 @@ void App::loop() {
|
|||||||
|
|
||||||
wifi.loop();
|
wifi.loop();
|
||||||
web.loop();
|
web.loop();
|
||||||
|
updateScheduleState();
|
||||||
|
updateAmbientDimming();
|
||||||
|
|
||||||
// Show WiFi status on display during connection attempts
|
// Show WiFi status on display during connection attempts
|
||||||
bool currentConnected = wifi.connected();
|
bool currentConnected = wifi.connected();
|
||||||
if (currentConnected != lastConnected) {
|
if (currentConnected != lastConnected) {
|
||||||
if (currentConnected) {
|
if (currentConnected) {
|
||||||
Serial.println("[App] WiFi connected - showing on display");
|
Serial.println("[App] WiFi connected - showing on display");
|
||||||
|
if (!isNightMode) {
|
||||||
display.showStatus("WiFi Connected!", wifi.ssid().c_str());
|
display.showStatus("WiFi Connected!", wifi.ssid().c_str());
|
||||||
delay(2000);
|
delay(2000);
|
||||||
|
}
|
||||||
} else if (wifi.mode() == NET_STA) {
|
} else if (wifi.mode() == NET_STA) {
|
||||||
Serial.println("[App] WiFi disconnected");
|
Serial.println("[App] WiFi disconnected");
|
||||||
if (millis() - lastDisplayUpdate > 5000) {
|
if (!isNightMode && millis() - lastDisplayUpdate > 5000) {
|
||||||
display.showStatus("WiFi", "Connecting...");
|
display.showStatus("WiFi", "Connecting...");
|
||||||
lastDisplayUpdate = millis();
|
lastDisplayUpdate = millis();
|
||||||
}
|
}
|
||||||
@@ -101,7 +242,9 @@ void App::loop() {
|
|||||||
|
|
||||||
moisture.loop();
|
moisture.loop();
|
||||||
battery.loop();
|
battery.loop();
|
||||||
|
if (!isNightMode) {
|
||||||
face.loop(moisture, battery);
|
face.loop(moisture, battery);
|
||||||
|
}
|
||||||
|
|
||||||
// Webhook events on state transitions
|
// Webhook events on state transitions
|
||||||
FaceRenderer::Mood m = face.mood();
|
FaceRenderer::Mood m = face.mood();
|
||||||
|
|||||||
40
src/sensors/AmbientLightSensor.cpp
Normal file
40
src/sensors/AmbientLightSensor.cpp
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
#include "AmbientLightSensor.h"
|
||||||
|
#include <math.h>
|
||||||
|
|
||||||
|
void AmbientLightSensor::begin() {
|
||||||
|
_ok = _veml.begin();
|
||||||
|
if (!_ok) {
|
||||||
|
Serial.println("[Ambient] VEML7700 not detected");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_veml.setGain(VEML7700_GAIN_1);
|
||||||
|
_veml.setIntegrationTime(VEML7700_IT_100MS);
|
||||||
|
|
||||||
|
_lux = _veml.readLux();
|
||||||
|
if (!isfinite(_lux) || _lux < 0.0f) _lux = 0.0f;
|
||||||
|
_filteredLux = _lux;
|
||||||
|
_nextMs = millis();
|
||||||
|
_lastUpdateMs = millis();
|
||||||
|
|
||||||
|
Serial.print("[Ambient] VEML7700 ready, initial lux: ");
|
||||||
|
Serial.println(_lux, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
void AmbientLightSensor::loop() {
|
||||||
|
if (!_ok) return;
|
||||||
|
|
||||||
|
unsigned long now = millis();
|
||||||
|
if ((long)(now - _nextMs) < 0) return;
|
||||||
|
_nextMs = now + INTERVAL_MS;
|
||||||
|
|
||||||
|
float rawLux = _veml.readLux();
|
||||||
|
if (!isfinite(rawLux) || rawLux < 0.0f) return;
|
||||||
|
|
||||||
|
_lux = rawLux;
|
||||||
|
|
||||||
|
// Simple low-pass filter to avoid display brightness flicker.
|
||||||
|
const float alpha = 0.2f;
|
||||||
|
_filteredLux = (_filteredLux * (1.0f - alpha)) + (_lux * alpha);
|
||||||
|
_lastUpdateMs = now;
|
||||||
|
}
|
||||||
25
src/sensors/AmbientLightSensor.h
Normal file
25
src/sensors/AmbientLightSensor.h
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <Arduino.h>
|
||||||
|
#include <Adafruit_VEML7700.h>
|
||||||
|
|
||||||
|
class AmbientLightSensor {
|
||||||
|
public:
|
||||||
|
void begin();
|
||||||
|
void loop();
|
||||||
|
|
||||||
|
bool available() const { return _ok; }
|
||||||
|
float lux() const { return _lux; }
|
||||||
|
float filteredLux() const { return _filteredLux; }
|
||||||
|
unsigned long lastUpdateMs() const { return _lastUpdateMs; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
static constexpr unsigned long INTERVAL_MS = 1000;
|
||||||
|
|
||||||
|
Adafruit_VEML7700 _veml;
|
||||||
|
bool _ok = false;
|
||||||
|
float _lux = 0.0f;
|
||||||
|
float _filteredLux = 0.0f;
|
||||||
|
unsigned long _nextMs = 0;
|
||||||
|
unsigned long _lastUpdateMs = 0;
|
||||||
|
};
|
||||||
@@ -4,11 +4,14 @@ void Display::begin() {
|
|||||||
Wire.begin(PIN_SDA, PIN_SCL);
|
Wire.begin(PIN_SDA, PIN_SCL);
|
||||||
_ok = _oled.begin(SSD1306_SWITCHCAPVCC, OLED_ADDR, true, false);
|
_ok = _oled.begin(SSD1306_SWITCHCAPVCC, OLED_ADDR, true, false);
|
||||||
if (!_ok) return;
|
if (!_ok) return;
|
||||||
|
_displayEnabled = true;
|
||||||
|
setContrast(_contrast);
|
||||||
bootAnimation();
|
bootAnimation();
|
||||||
}
|
}
|
||||||
|
|
||||||
void Display::showStatus(const String& line1, const String& line2) {
|
void Display::showStatus(const String& line1, const String& line2) {
|
||||||
if (!_ok) return;
|
if (!_ok) return;
|
||||||
|
if (!_displayEnabled) setDisplayEnabled(true);
|
||||||
_oled.clearDisplay();
|
_oled.clearDisplay();
|
||||||
_oled.setTextSize(1);
|
_oled.setTextSize(1);
|
||||||
_oled.setTextColor(1);
|
_oled.setTextColor(1);
|
||||||
@@ -19,6 +22,28 @@ void Display::showStatus(const String& line1, const String& line2) {
|
|||||||
_oled.display();
|
_oled.display();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Display::setContrast(uint8_t contrast) {
|
||||||
|
if (!_ok) return;
|
||||||
|
_contrast = contrast;
|
||||||
|
_oled.ssd1306_command(SSD1306_SETCONTRAST);
|
||||||
|
_oled.ssd1306_command(_contrast);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Display::setDisplayEnabled(bool enabled) {
|
||||||
|
if (!_ok) return;
|
||||||
|
if (_displayEnabled == enabled) return;
|
||||||
|
|
||||||
|
_displayEnabled = enabled;
|
||||||
|
if (enabled) {
|
||||||
|
_oled.ssd1306_command(SSD1306_DISPLAYON);
|
||||||
|
setContrast(_contrast);
|
||||||
|
} else {
|
||||||
|
_oled.clearDisplay();
|
||||||
|
_oled.display();
|
||||||
|
_oled.ssd1306_command(SSD1306_DISPLAYOFF);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void Display::bootAnimation() {
|
void Display::bootAnimation() {
|
||||||
if (!_ok) return;
|
if (!_ok) return;
|
||||||
|
|
||||||
|
|||||||
@@ -8,10 +8,14 @@ class Display {
|
|||||||
public:
|
public:
|
||||||
void begin();
|
void begin();
|
||||||
Adafruit_SSD1306& oled() { return _oled; }
|
Adafruit_SSD1306& oled() { return _oled; }
|
||||||
|
bool ok() const { return _ok; }
|
||||||
|
|
||||||
void showStatus(const String& line1, const String& line2);
|
void showStatus(const String& line1, const String& line2);
|
||||||
void bootAnimation();
|
void bootAnimation();
|
||||||
void drawBatteryIcon(int x, int y, int percent, bool blink, bool charging);
|
void drawBatteryIcon(int x, int y, int percent, bool blink, bool charging);
|
||||||
|
void setContrast(uint8_t contrast);
|
||||||
|
void setDisplayEnabled(bool enabled);
|
||||||
|
bool displayEnabled() const { return _displayEnabled; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
static constexpr int PIN_SDA = 6;
|
static constexpr int PIN_SDA = 6;
|
||||||
@@ -19,4 +23,6 @@ private:
|
|||||||
static constexpr uint8_t OLED_ADDR = 0x3C; // try 0x3D if blank
|
static constexpr uint8_t OLED_ADDR = 0x3C; // try 0x3D if blank
|
||||||
Adafruit_SSD1306 _oled = Adafruit_SSD1306(128, 64, &Wire, -1);
|
Adafruit_SSD1306 _oled = Adafruit_SSD1306(128, 64, &Wire, -1);
|
||||||
bool _ok = false;
|
bool _ok = false;
|
||||||
|
bool _displayEnabled = true;
|
||||||
|
uint8_t _contrast = 0xCF;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -16,6 +16,11 @@ int8_t FaceRenderer::randRangeI8(int8_t lo, int8_t hi) {
|
|||||||
return (int8_t)(lo + random((int)(hi - lo + 1)));
|
return (int8_t)(lo + random((int)(hi - lo + 1)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void FaceRenderer::setRoutineAnimation(RoutineAnim anim, uint16_t progressPermille) {
|
||||||
|
_routineAnim = anim;
|
||||||
|
_routineProgressPermille = progressPermille > 1000 ? 1000 : progressPermille;
|
||||||
|
}
|
||||||
|
|
||||||
void FaceRenderer::begin(Display& display, Settings& settings) {
|
void FaceRenderer::begin(Display& display, Settings& settings) {
|
||||||
_display = &display;
|
_display = &display;
|
||||||
_settings = &settings;
|
_settings = &settings;
|
||||||
@@ -38,6 +43,11 @@ void FaceRenderer::loop(const MoistureSensor& moisture, const BatterySensor& bat
|
|||||||
updateDeathMode(now);
|
updateDeathMode(now);
|
||||||
updateBatteryLowMode(battery.percent());
|
updateBatteryLowMode(battery.percent());
|
||||||
|
|
||||||
|
if (_routineAnim != ROUTINE_NONE) {
|
||||||
|
renderRoutine(now, battery);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (_deadMode) {
|
if (_deadMode) {
|
||||||
renderDead(now, battery);
|
renderDead(now, battery);
|
||||||
return;
|
return;
|
||||||
@@ -235,6 +245,55 @@ void FaceRenderer::renderNormal(unsigned long now, const BatterySensor& battery)
|
|||||||
d.display();
|
d.display();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void FaceRenderer::renderRoutine(unsigned long now, const BatterySensor& battery) {
|
||||||
|
auto &d = _display->oled();
|
||||||
|
d.clearDisplay();
|
||||||
|
|
||||||
|
if (_routineAnim == ROUTINE_SLEEPING_SOON) {
|
||||||
|
// Progressively lower eyelids over 5 minutes.
|
||||||
|
int close = (_routineProgressPermille * 12) / 1000; // 0..12
|
||||||
|
int pupilY = (_routineProgressPermille > 700) ? 2 : 0;
|
||||||
|
|
||||||
|
d.fillCircle(40, 24, 12, 1);
|
||||||
|
d.fillCircle(88, 24, 12, 1);
|
||||||
|
d.fillCircle(40, 24 + pupilY, 3, 0);
|
||||||
|
d.fillCircle(88, 24 + pupilY, 3, 0);
|
||||||
|
if (close > 0) {
|
||||||
|
d.fillRect(28, 12, 24, close, 0);
|
||||||
|
d.fillRect(76, 12, 24, close, 0);
|
||||||
|
}
|
||||||
|
if (close > 6) {
|
||||||
|
d.drawLine(28, 24, 52, 24, 1);
|
||||||
|
d.drawLine(76, 24, 100, 24, 1);
|
||||||
|
}
|
||||||
|
d.drawLine(26, 14, 48, 16, 1);
|
||||||
|
d.drawLine(80, 16, 102, 14, 1);
|
||||||
|
d.drawLine(52, 48, 76, 48, 1);
|
||||||
|
drawSleepyZs(now, (uint8_t)(1 + ((_routineProgressPermille * 3) / 1000)));
|
||||||
|
} else {
|
||||||
|
// Wake-up: eyes open wider and smile grows over 5 minutes.
|
||||||
|
int open = 4 + (_routineProgressPermille * 8) / 1000; // pupil travel
|
||||||
|
int pupilY = 4 - (_routineProgressPermille * 4) / 1000; // starts sleepy, rises to center
|
||||||
|
|
||||||
|
d.fillCircle(40, 24, 12, 1);
|
||||||
|
d.fillCircle(88, 24, 12, 1);
|
||||||
|
d.fillCircle(40, 24 + pupilY, open / 3, 0);
|
||||||
|
d.fillCircle(88, 24 + pupilY, open / 3, 0);
|
||||||
|
|
||||||
|
// Bright eyebrows + growing smile
|
||||||
|
d.drawLine(26, 14, 48, 11, 1);
|
||||||
|
d.drawLine(80, 11, 102, 14, 1);
|
||||||
|
int smileLift = (_routineProgressPermille * 3) / 1000;
|
||||||
|
for (int i = 0; i < 4; i++) {
|
||||||
|
d.drawLine(55 + i, 47 + i / 2 - smileLift, 73 - i, 47 + i / 2 - smileLift, 1);
|
||||||
|
}
|
||||||
|
drawSunRise(now, (uint8_t)(1 + ((_routineProgressPermille * 4) / 1000)));
|
||||||
|
}
|
||||||
|
|
||||||
|
_display->drawBatteryIcon(110, 2, battery.percent(), battery.shouldBlink(), battery.isCharging());
|
||||||
|
d.display();
|
||||||
|
}
|
||||||
|
|
||||||
/* --- Drawing helpers --- */
|
/* --- Drawing helpers --- */
|
||||||
|
|
||||||
void FaceRenderer::drawEyesOpen(int dx, int dy) {
|
void FaceRenderer::drawEyesOpen(int dx, int dy) {
|
||||||
@@ -350,3 +409,34 @@ void FaceRenderer::drawBigWaterText(unsigned long now) {
|
|||||||
d.setCursor(x < 0 ? 0 : x, 56);
|
d.setCursor(x < 0 ? 0 : x, 56);
|
||||||
d.print(msg);
|
d.print(msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void FaceRenderer::drawSleepyZs(unsigned long now, uint8_t count) {
|
||||||
|
auto &d = _display->oled();
|
||||||
|
uint8_t phase = (now / 400UL) % 8UL;
|
||||||
|
for (uint8_t i = 0; i < count; i++) {
|
||||||
|
int x = 98 + i * 8 - (int)phase;
|
||||||
|
int y = 22 - i * 7;
|
||||||
|
if (x < 84 || y < 2) continue;
|
||||||
|
d.drawLine(x, y, x + 4, y, 1);
|
||||||
|
d.drawLine(x + 4, y, x, y + 5, 1);
|
||||||
|
d.drawLine(x, y + 5, x + 4, y + 5, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void FaceRenderer::drawSunRise(unsigned long now, uint8_t level) {
|
||||||
|
auto &d = _display->oled();
|
||||||
|
int cx = 12;
|
||||||
|
int cy = 52;
|
||||||
|
int r = 5;
|
||||||
|
d.drawCircle(cx, cy, r, 1);
|
||||||
|
d.drawLine(cx - 10, cy + 6, cx + 10, cy + 6, 1);
|
||||||
|
if (level >= 2) {
|
||||||
|
d.drawLine(cx, cy - 10, cx, cy - 7, 1);
|
||||||
|
d.drawLine(cx - 8, cy - 4, cx - 5, cy - 3, 1);
|
||||||
|
d.drawLine(cx + 8, cy - 4, cx + 5, cy - 3, 1);
|
||||||
|
}
|
||||||
|
if (level >= 4 && ((now / 300UL) % 2UL == 0UL)) {
|
||||||
|
d.drawLine(cx - 11, cy - 10, cx - 8, cy - 7, 1);
|
||||||
|
d.drawLine(cx + 11, cy - 10, cx + 8, cy - 7, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,9 +8,11 @@
|
|||||||
class FaceRenderer {
|
class FaceRenderer {
|
||||||
public:
|
public:
|
||||||
enum Mood { HAPPY, DRY, TOO_WET };
|
enum Mood { HAPPY, DRY, TOO_WET };
|
||||||
|
enum RoutineAnim { ROUTINE_NONE, ROUTINE_SLEEPING_SOON, ROUTINE_WAKING_UP };
|
||||||
|
|
||||||
void begin(Display& display, Settings& settings);
|
void begin(Display& display, Settings& settings);
|
||||||
void loop(const MoistureSensor& moisture, const BatterySensor& battery);
|
void loop(const MoistureSensor& moisture, const BatterySensor& battery);
|
||||||
|
void setRoutineAnimation(RoutineAnim anim, uint16_t progressPermille);
|
||||||
|
|
||||||
bool isDeadMode() const { return _deadMode; }
|
bool isDeadMode() const { return _deadMode; }
|
||||||
Mood mood() const { return _mood; }
|
Mood mood() const { return _mood; }
|
||||||
@@ -51,6 +53,9 @@ private:
|
|||||||
int8_t _shakeX = 0, _shakeY = 0;
|
int8_t _shakeX = 0, _shakeY = 0;
|
||||||
unsigned long _nextShakeMs = 0;
|
unsigned long _nextShakeMs = 0;
|
||||||
|
|
||||||
|
RoutineAnim _routineAnim = ROUTINE_NONE;
|
||||||
|
uint16_t _routineProgressPermille = 0;
|
||||||
|
|
||||||
void updateMood(int moisturePct);
|
void updateMood(int moisturePct);
|
||||||
void updateDeathMode(unsigned long now);
|
void updateDeathMode(unsigned long now);
|
||||||
void updateBatteryLowMode(int batteryPercent);
|
void updateBatteryLowMode(int batteryPercent);
|
||||||
@@ -61,6 +66,7 @@ private:
|
|||||||
void renderDead(unsigned long now, const BatterySensor& battery);
|
void renderDead(unsigned long now, const BatterySensor& battery);
|
||||||
void renderBatteryLow(unsigned long now, const BatterySensor& battery);
|
void renderBatteryLow(unsigned long now, const BatterySensor& battery);
|
||||||
void renderNormal(unsigned long now, const BatterySensor& battery);
|
void renderNormal(unsigned long now, const BatterySensor& battery);
|
||||||
|
void renderRoutine(unsigned long now, const BatterySensor& battery);
|
||||||
|
|
||||||
void drawEyesOpen(int pupilDx, int pupilDy);
|
void drawEyesOpen(int pupilDx, int pupilDy);
|
||||||
void drawEyesClosed();
|
void drawEyesClosed();
|
||||||
@@ -77,6 +83,8 @@ private:
|
|||||||
void drawSilly(uint8_t v);
|
void drawSilly(uint8_t v);
|
||||||
void drawXEye(int cx, int cy);
|
void drawXEye(int cx, int cy);
|
||||||
void drawBigWaterText(unsigned long now);
|
void drawBigWaterText(unsigned long now);
|
||||||
|
void drawSleepyZs(unsigned long now, uint8_t count);
|
||||||
|
void drawSunRise(unsigned long now, uint8_t level);
|
||||||
|
|
||||||
unsigned long randRange(unsigned long lo, unsigned long hi);
|
unsigned long randRange(unsigned long lo, unsigned long hi);
|
||||||
int8_t randRangeI8(int8_t lo, int8_t hi);
|
int8_t randRangeI8(int8_t lo, int8_t hi);
|
||||||
|
|||||||
Reference in New Issue
Block a user