Enclosure files + firmware

This commit is contained in:
Salim Benbouziyane
2024-12-27 16:11:21 -05:00
parent a0ae88370c
commit 5fd10e2f6b
62 changed files with 3587 additions and 1 deletions

View File

@@ -0,0 +1,82 @@
#include "Animation.h"
Animation::Animation(Adafruit_SSD1306* display) : oled(display), animationRunning(false), playInReverse(false) {}
void Animation::start(const byte* frames, int frameCount, bool loop, bool reverse, unsigned long durationMs, int width, int height) {
animationFrames = frames;
totalFrames = frameCount;
loopAnimation = loop;
playInReverse = reverse; // Set reverse playback flag
animationRunning = true;
// Initialize current frame correctly based on direction
currentFrame = playInReverse ? totalFrames - 1 : 0;
frameWidth = width;
frameHeight = height;
frameDelay = DEFAULT_FRAME_DELAY;
if (durationMs == 0) {
animationDuration = totalFrames * frameDelay;
} else {
animationDuration = durationMs;
}
animationStartTime = millis();
lastFrameTime = millis();
frameX = (oled->width() - frameWidth) / 2;
frameY = (oled->height() - frameHeight) / 2;
oled->clearDisplay();
oled->drawBitmap(frameX, frameY, &animationFrames[currentFrame * 288], frameWidth, frameHeight, 1);
oled->display();
}
void Animation::update() {
if (!animationRunning) return;
unsigned long currentTime = millis();
if (currentTime - animationStartTime >= animationDuration) {
animationRunning = false;
return;
}
// Check if it's time to advance to the next frame
if (currentTime - lastFrameTime >= frameDelay) {
lastFrameTime = currentTime;
// Adjust current frame based on direction
if (playInReverse) {
currentFrame--;
if (currentFrame < 0) {
if (loopAnimation) {
currentFrame = totalFrames - 1; // Wrap around to last frame
} else {
animationRunning = false;
return;
}
}
} else {
currentFrame++;
if (currentFrame >= totalFrames) {
if (loopAnimation) {
currentFrame = 0; // Wrap around to first frame
} else {
animationRunning = false;
return;
}
}
}
// Display the current frame
oled->clearDisplay();
oled->drawBitmap(frameX, frameY, &animationFrames[currentFrame * 288], frameWidth, frameHeight, 1);
oled->display();
}
}
bool Animation::isRunning() {
return animationRunning;
}

View File

@@ -0,0 +1,47 @@
#include "StateMachine.h"
// Global state machine instance
StateMachine stateMachine;
// Initialize static states
AdjustState StateMachine::adjustState;
SleepState StateMachine::sleepState;
DoneState StateMachine::doneState;
IdleState StateMachine::idleState;
PausedState StateMachine::pausedState;
ProvisionState StateMachine::provisionState;
ResetState StateMachine::resetState;
StartupState StateMachine::startupState;
TimerState StateMachine::timerState;
StateMachine::StateMachine() {
currentState = &startupState; // Start with StartupState
stateMutex = xSemaphoreCreateMutex(); // Initialize the mutex
}
// Clean up the state and delete the mutex
StateMachine::~StateMachine() {
if (stateMutex != NULL) {
vSemaphoreDelete(stateMutex); // Delete the mutex
}
}
void StateMachine::changeState(State* newState) {
// Lock the mutex
if (xSemaphoreTake(stateMutex, portMAX_DELAY) == pdTRUE) {
transition = true;
if (currentState != nullptr) {
currentState->exit();
}
currentState = newState; // Assign the new state (static state)
currentState->enter();
transition = false;
xSemaphoreGive(stateMutex); // Release the mutex
}
}
void StateMachine::update() {
if (!transition && currentState != nullptr) {
currentState->update(); // Call update on the current state
}
}

View File

@@ -0,0 +1,458 @@
#include "controllers/DisplayController.h"
#include "fonts/Picopixel.h"
#include "fonts/Org_01.h"
#include "bitmaps.h"
DisplayController::DisplayController(uint8_t oledWidth, uint8_t oledHeight, uint8_t oledAddress)
: oled(oledWidth, oledHeight, &Wire, -1), animation(&oled) {}
void DisplayController::begin() {
if (!oled.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
Serial.println(F("SSD1306 allocation failed"));
for (;;); // Loop forever if initialization fails
}
// oled.ssd1306_command(SSD1306_SETCONTRAST);
// oled.ssd1306_command(128);
oled.clearDisplay();
oled.display();
Serial.println("DisplayController initialized.");
}
void DisplayController::drawSplashScreen() {
oled.clearDisplay();
oled.drawBitmap(16, 3, focusdial_logo, 99, 45, 1);
oled.setTextColor(1);
oled.setTextSize(1);
oled.setFont(&Picopixel);
oled.setCursor(21, 60);
oled.print("YOUTUBE/ @SALIMBENBOUZ");
oled.display();
}
void DisplayController::drawIdleScreen(int duration, bool wifi) {
if (isAnimationRunning()) return;
static unsigned long lastBlinkTime = 0;
static bool blinkState = true;
unsigned long currentTime = millis();
// Toggle blink state if WiFi is off
if (!wifi && (currentTime - lastBlinkTime >= 500)) {
blinkState = !blinkState;
lastBlinkTime = currentTime;
}
oled.clearDisplay();
// "PRESS TO START"
oled.setFont(&Picopixel);
oled.setTextSize(1);
oled.setTextColor(1);
oled.setCursor(40, 58);
oled.print("PRESS TO START");
oled.drawRoundRect(35, 51, 60, 11, 1, 1);
// Display WiFi icon based on WiFi state
if (wifi) {
oled.drawBitmap(70, 3, icon_wifi_on, 5, 5, 1);
oled.setCursor(54, 7);
oled.print("WIFI");
} else if (blinkState) {
oled.drawBitmap(70, 3, icon_wifi_off, 5, 5, 1);
oled.setCursor(54, 7);
oled.print("WIFI");
}
char left[3], right[3];
int xLeft = 1;
int xRight = 73;
if (duration < 60) {
sprintf(left, "%02d", duration);
strcpy(right, "00");
} else {
int hours = duration / 60;
int minutes = duration % 60;
sprintf(left, "%02d", hours);
sprintf(right, "%02d", minutes);
}
// Adjust position if the first character is '1'
if (left[0] == '1') {
xLeft += 20;
}
if (right[0] == '1') {
xRight += 20;
}
oled.setTextSize(5);
oled.setFont(&Org_01);
oled.setCursor(xLeft, 36);
oled.print(left);
oled.setCursor(xRight, 36);
oled.print(right);
// Separator dots
oled.fillRect(62, 21, 5, 5, 1);
oled.fillRect(62, 31, 5, 5, 1);
oled.display();
}
void DisplayController::drawTimerScreen(int remainingSeconds) {
if (isAnimationRunning()) return;
oled.clearDisplay();
if (remainingSeconds < 0) {
remainingSeconds = 0;
}
int hours = remainingSeconds / 3600;
int minutes = (remainingSeconds % 3600) / 60;
int seconds = remainingSeconds % 60;
char left[3], right[3], secondsStr[3];
int xLeft = 1;
int xRight = 73;
// Format left and right
if (hours > 0) {
sprintf(left, "%02d", hours);
sprintf(right, "%02d", minutes);
} else {
sprintf(left, "%02d", minutes);
sprintf(right, "%02d", seconds);
}
// Adjust position if the first character is '1'
if (left[0] == '1') {
xLeft += 20;
}
if (right[0] == '1') {
xRight += 20;
}
// Draw the left value (hours or minutes)
oled.setTextColor(1);
oled.setTextSize(5);
oled.setFont(&Org_01);
oled.setCursor(xLeft, 36);
oled.print(left);
// Draw the right value (minutes or seconds)
oled.setCursor(xRight, 36);
oled.print(right);
// Separator dots
oled.fillRect(62, 31, 5, 5, 1);
oled.fillRect(62, 21, 5, 5, 1);
sprintf(secondsStr, "%02d", seconds);
int xSeconds = 54;
if (secondsStr[0] == '1') {
xSeconds += 8; // Offset by 8 if the first char is '1'
}
oled.setTextSize(2);
oled.setCursor(xSeconds, 58);
oled.print(secondsStr);
// Draw icons and labels
oled.drawBitmap(61, 3, icon_star, 7, 7, 1);
oled.setTextSize(1);
oled.setCursor(27, 54);
oled.print(hours > 0 ? "H" : "M");
oled.setCursor(98, 54);
oled.print(hours > 0 ? "M" : "S");
oled.display();
}
void DisplayController::drawPausedScreen(int remainingSeconds) {
if (isAnimationRunning()) return;
oled.clearDisplay();
if (remainingSeconds < 0) {
remainingSeconds = 0;
}
int hours = remainingSeconds / 3600;
int minutes = (remainingSeconds % 3600) / 60;
int seconds = remainingSeconds % 60;
char left[3], right[3];
int xLeft = 1;
int xRight = 73;
// Format left and right
if (hours > 0) {
sprintf(left, "%02d", hours);
sprintf(right, "%02d", minutes);
} else {
sprintf(left, "%02d", minutes);
sprintf(right, "%02d", seconds);
}
// Adjust position if the first character is '1'
if (left[0] == '1') {
xLeft += 20;
}
if (right[0] == '1') {
xRight += 20;
}
if ((millis() / 400) % 2 == 0) {
oled.setTextColor(1);
oled.setTextSize(5);
oled.setFont(&Org_01);
oled.setCursor(xLeft, 36);
oled.print(left);
oled.setCursor(xRight, 36);
oled.print(right);
oled.fillRect(62, 31, 5, 5, 1);
oled.fillRect(62, 22, 5, 5, 1);
oled.setFont(&Org_01);
oled.setTextSize(1);
oled.setCursor(27, 54);
oled.print(hours > 0 ? "H" : "M");
oled.setCursor(98, 54);
oled.print(hours > 0 ? "M" : "S");
}
// Draw label and icon
oled.drawRoundRect(47, 51, 35, 11, 1, 1);
oled.setTextColor(1);
oled.setTextSize(1);
oled.setFont(&Picopixel);
oled.setCursor(53, 58);
oled.print("PAUSED");
oled.drawBitmap(60, 2, icon_pause, 9, 9, 1);
oled.display();
}
void DisplayController::drawResetScreen(bool resetSelected) {
if (isAnimationRunning()) return;
oled.clearDisplay();
// Static UI elements
oled.setTextColor(1);
oled.setTextSize(2);
oled.setFont(&Picopixel);
oled.setCursor(54, 15);
oled.print("RESET");
oled.setTextSize(1);
oled.setCursor(20, 30);
oled.print("ALL STORED SETTINGS WILL ");
oled.setCursor(21, 40);
oled.print("BE PERMANENTLY ERASED.");
oled.drawBitmap(35, 4, icon_reset, 13, 16, 1);
// Change only the rectangle fill and text color based on selection
if (resetSelected) {
// "RESET" filled, "CANCEL" outlined
oled.fillRoundRect(67, 49, 37, 11, 1, 1);
oled.setTextColor(0);
oled.setCursor(76, 56);
oled.print("RESET");
oled.drawRoundRect(24, 49, 37, 11, 1, 1);
oled.setTextColor(1);
oled.setCursor(31, 56);
oled.print("CANCEL");
} else {
// "CANCEL" filled, "RESET" outlined
oled.fillRoundRect(24, 49, 37, 11, 1, 1);
oled.setTextColor(0);
oled.setCursor(31, 56);
oled.print("CANCEL");
oled.drawRoundRect(67, 49, 37, 11, 1, 1);
oled.setTextColor(1);
oled.setCursor(76, 56);
oled.print("RESET");
}
oled.display();
}
void DisplayController::drawDoneScreen() {
if (isAnimationRunning()) return;
static unsigned long lastBlinkTime = 0;
static bool blinkState = true;
unsigned long currentTime = millis();
// Toggle blink every 500 ms
if (currentTime - lastBlinkTime >= 500) {
blinkState = !blinkState;
lastBlinkTime = currentTime;
}
oled.clearDisplay();
if (blinkState) {
oled.setTextColor(1);
oled.setTextSize(5);
oled.setFont(&Org_01);
oled.setCursor(1, 36);
oled.print("00");
oled.setCursor(73, 36);
oled.print("00");
oled.fillRect(62, 31, 5, 5, 1);
oled.fillRect(62, 21, 5, 5, 1);
}
// Draw label and icon
oled.fillRoundRect(46, 51, 35, 11, 1, 1);
oled.setTextColor(0);
oled.setTextSize(1);
oled.setFont(&Picopixel);
oled.setCursor(56, 58);
oled.print("DONE");
oled.drawBitmap(61, 3, icon_star, 7, 7, 1);
oled.display();
}
void DisplayController::drawAdjustScreen(int duration) {
if (isAnimationRunning()) return;
oled.clearDisplay();
oled.setTextColor(1);
oled.setTextSize(4);
oled.setFont(&Org_01);
int hours = duration / 60;
int minutes = duration % 60;
char hourStr[3];
char minuteStr[3];
// Format hour and minute strings with leading zeros
sprintf(hourStr, "%02d", hours);
sprintf(minuteStr, "%02d", minutes);
// Default positions for hours and minutes
int xHour = 13;
int xMinute = 72;
// Check the first character and adjust position if '1'
if (hourStr[0] == '1') {
xHour += 15;
}
if (minuteStr[0] == '1') {
xMinute += 15;
}
// Display hours
oled.setCursor(xHour, 37);
oled.print(hourStr);
// Display minutes
oled.setCursor(xMinute, 37);
oled.print(minuteStr);
// Display labels
oled.setTextSize(1);
oled.setCursor(26, 55);
oled.print("HRS");
oled.setCursor(86, 55);
oled.print("MIN");
// Additional UI elements
oled.drawBitmap(0, 12, image_change_left, 7, 40, 1);
oled.drawRoundRect(36, 1, 57, 11, 1, 1);
oled.drawBitmap(121, 12, image_change_right, 7, 40, 1);
oled.setFont(&Picopixel);
oled.setCursor(41, 8);
oled.print("PRESS TO SAVE");
oled.drawBitmap(103, 3, icon_arrow_down, 5, 7, 1);
oled.drawBitmap(21, 3, icon_arrow_down, 5, 7, 1);
oled.display();
}
void DisplayController::drawProvisionScreen() {
if (isAnimationRunning()) return;
oled.clearDisplay();
oled.setTextColor(1);
oled.setTextSize(1);
oled.setFont(&Picopixel);
oled.setCursor(12, 38);
oled.print("PLEASE CONNECT TO BLUETOOTH");
oled.setCursor(14, 48);
oled.print("AND THIS FOCUSDIAL NETWORK");
oled.setCursor(35, 58);
oled.print("TO PROVISION WIFI");
oled.drawBitmap(39, 4, provision_logo, 51, 23, 1);
oled.display();
}
void DisplayController::clear() {
oled.clearDisplay();
oled.display();
}
void DisplayController::showAnimation(const byte frames[][288], int frameCount, bool loop, bool reverse, unsigned long durationMs, int width, int height) {
animation.start(&frames[0][0], frameCount, loop, reverse, durationMs, width, height); // Pass array as pointer
}
void DisplayController::updateAnimation() {
animation.update();
}
bool DisplayController::isAnimationRunning() {
return animation.isRunning();
}
void DisplayController::showConfirmation() {
showAnimation(animation_tick, 20);
}
void DisplayController::showCancel() {
showAnimation(animation_cancel, 18, false, true);
}
void DisplayController::showReset() {
showAnimation(animation_reset, 28, true, false);
}
void DisplayController::showConnected() {
showAnimation(animation_wifi, 28);
}
void DisplayController::showTimerStart() {
showAnimation(animation_timer_start, 20, false, true);
}
void DisplayController::showTimerDone() {
showAnimation(animation_timer_start, 20);
}
void DisplayController::showTimerPause() {
showAnimation(animation_resume, 18, false, true);
}
void DisplayController::showTimerResume() {
showAnimation(animation_resume, 18);
}

View File

@@ -0,0 +1,141 @@
#include "controllers/InputController.h"
#include <Arduino.h>
static InputController *instancePtr = nullptr; // Global pointer for the ISR
void InputController::handleEncoderInterrupt()
{
if (instancePtr)
{
instancePtr->encoder.tick();
}
}
void InputController::handleButtonInterrupt()
{
if (instancePtr)
{
instancePtr->button.tick();
}
}
InputController::InputController(uint8_t buttonPin, uint8_t encoderPinA, uint8_t encoderPinB)
: button(buttonPin, true),
encoder(encoderPinA, encoderPinB, RotaryEncoder::LatchMode::TWO03),
lastPosition(0),
buttonPin(buttonPin),
encoderPinA(encoderPinA),
encoderPinB(encoderPinB)
{
// Attach click, double-click, and long-press handlers using OneButton library
button.attachClick([](void *scope)
{ static_cast<InputController *>(scope)->onButtonClick(); }, this);
button.attachDoubleClick([](void *scope)
{ static_cast<InputController *>(scope)->onButtonDoubleClick(); }, this);
button.attachLongPressStart([](void *scope)
{ static_cast<InputController *>(scope)->onButtonLongPress(); }, this);
instancePtr = this; // Set the global instance pointer to this instance
}
void InputController::begin()
{
button.setDebounceMs(20);
button.setClickMs(150);
button.setPressMs(400);
lastPosition = encoder.getPosition();
pinMode(buttonPin, INPUT_PULLUP);
pinMode(encoderPinA, INPUT_PULLUP);
pinMode(encoderPinB, INPUT_PULLUP);
// Set up interrupts for encoder handling
attachInterrupt(digitalPinToInterrupt(encoderPinA), handleEncoderInterrupt, CHANGE);
attachInterrupt(digitalPinToInterrupt(encoderPinB), handleEncoderInterrupt, CHANGE);
// Set up interrupt for button handling
attachInterrupt(digitalPinToInterrupt(buttonPin), handleButtonInterrupt, CHANGE); // Interrupt on button state change
}
void InputController::update()
{
button.tick();
encoder.tick();
// Check encoder position and calculate delta
int currentPosition = encoder.getPosition();
int delta = currentPosition - lastPosition;
if (delta != 0)
{
onEncoderRotate(delta);
lastPosition = currentPosition;
}
}
// Register state-specific handlers
void InputController::onPressHandler(std::function<void()> handler)
{
pressHandler = handler;
}
void InputController::onDoublePressHandler(std::function<void()> handler)
{
doublePressHandler = handler;
}
void InputController::onLongPressHandler(std::function<void()> handler)
{
longPressHandler = handler;
}
void InputController::onEncoderRotateHandler(std::function<void(int delta)> handler)
{
encoderRotateHandler = handler;
}
// Method to release all handlers
void InputController::releaseHandlers()
{
pressHandler = nullptr;
doublePressHandler = nullptr;
longPressHandler = nullptr;
encoderRotateHandler = nullptr;
button.reset(); // Reset button state machine
lastPosition = encoder.getPosition(); // Reset encoder position tracking
}
// Internal event handlers that call the registered state handlers
void InputController::onButtonClick()
{
if (pressHandler != nullptr)
{
pressHandler();
}
}
void InputController::onButtonDoubleClick()
{
if (doublePressHandler != nullptr)
{
doublePressHandler();
}
}
void InputController::onButtonLongPress()
{
if (longPressHandler != nullptr)
{
longPressHandler();
}
}
void InputController::onEncoderRotate(int delta)
{
if (encoderRotateHandler != nullptr)
{
encoderRotateHandler(delta); // Pass delta to the handler
}
}

View File

@@ -0,0 +1,241 @@
#include "controllers/LedController.h"
LEDController::LEDController(uint8_t ledPin, uint16_t numLeds, uint8_t brightness)
: leds(numLeds, ledPin),
numLeds(numLeds),
brightness(brightness),
currentAnimation(None),
lastUpdateTime(0),
currentStep(0),
currentCycle(0),
decayStarted(false) {}
void LEDController::begin()
{
leds.begin();
leds.setBrightness(brightness);
leds.show();
}
void LEDController::update()
{
switch (currentAnimation)
{
case FillAndDecay:
handleFillAndDecay();
break;
case Spinner:
handleSpinner();
break;
case Breath:
handleBreath();
break;
default:
break;
}
}
void LEDController::startFillAndDecay(uint32_t color, uint32_t totalDuration)
{
stopCurrentAnimation();
currentAnimation = FillAndDecay;
animationColor = color;
animationDuration = totalDuration;
currentStep = 0;
pixelIndex = 0;
brightnessLevel = brightness;
lastUpdateTime = millis();
}
void LEDController::setSpinner(uint32_t color, int cycles)
{
stopCurrentAnimation();
currentAnimation = Spinner;
animationColor = color;
animationCycles = cycles;
currentCycle = 0;
currentStep = 0;
lastUpdateTime = millis();
}
void LEDController::setBreath(uint32_t color, int cycles, bool endFilled, uint32_t speed)
{
stopCurrentAnimation();
currentAnimation = Breath;
animationColor = color;
animationCycles = cycles;
this->endFilled = endFilled;
animationSpeed = speed;
currentCycle = 0;
currentStep = 0;
lastUpdateTime = millis();
}
void LEDController::setSolid(uint32_t color)
{
stopCurrentAnimation();
leds.fill(color);
leds.show();
}
void LEDController::turnOff()
{
stopCurrentAnimation();
leds.clear();
leds.show();
}
uint32_t LEDController::scaleColor(uint32_t color, uint8_t brightnessLevel)
{
uint8_t r = (color >> 16 & 0xFF) * brightnessLevel / 255;
uint8_t g = (color >> 8 & 0xFF) * brightnessLevel / 255;
uint8_t b = (color & 0xFF) * brightnessLevel / 255;
return leds.Color(r, g, b);
}
void LEDController::handleFillAndDecay()
{
uint32_t fillDuration = 300; // Initial fill duration
uint32_t decayDuration = animationDuration - fillDuration;
uint32_t totalSteps = (numLeds + 1) * brightness;
uint32_t stepDuration = decayDuration / totalSteps;
if (currentStep < numLeds)
{
// Quick fill phase
uint32_t stepDurationFill = fillDuration / numLeds;
if (millis() - lastUpdateTime >= stepDurationFill)
{
leds.setPixelColor(currentStep, scaleColor(animationColor, brightness));
leds.show();
currentStep++;
lastUpdateTime = millis();
}
}
else
{
// Initialize decay phase
if (!decayStarted)
{
decayStarted = true;
pixelIndex = 1;
brightnessLevel = brightness;
lastUpdateTime = millis();
}
// Decay phase
if (millis() - lastUpdateTime >= stepDuration)
{
lastUpdateTime = millis();
if (brightnessLevel > 0)
{
brightnessLevel--;
leds.setPixelColor(pixelIndex, scaleColor(animationColor, brightnessLevel));
leds.show();
}
else
{
leds.setPixelColor(pixelIndex, 0);
leds.show();
pixelIndex++;
brightnessLevel = brightness;
}
if (pixelIndex > numLeds)
{
stopCurrentAnimation();
}
}
}
}
void LEDController::handleSpinner()
{
uint32_t stepDuration = 100;
if (millis() - lastUpdateTime >= stepDuration)
{
leds.clear();
for (int i = 0; i < numLeds; i++)
{
leds.setPixelColor((i + currentStep) % numLeds, scaleColor(animationColor, i * 255 / numLeds));
}
leds.show();
currentStep++;
lastUpdateTime = millis();
if (currentStep >= numLeds)
{
currentStep = 0;
currentCycle++;
if (animationCycles != -1 && currentCycle >= animationCycles)
{
stopCurrentAnimation();
}
}
}
}
void LEDController::handleBreath()
{
if (millis() - lastUpdateTime >= animationSpeed)
{
uint8_t fadeBrightness = (currentStep <= 127) ? currentStep * 2 : (255 - currentStep) * 2;
for (int i = 0; i < numLeds; i++)
{
leds.setPixelColor(i, scaleColor(animationColor, fadeBrightness));
}
leds.show();
currentStep++;
if (currentStep >= 255)
{
currentStep = 0;
currentCycle++;
// Adjust the number of cycles if `endFilled` is true
int effectiveCycles = animationCycles;
if (endFilled && effectiveCycles > 0)
{
effectiveCycles--;
}
if (effectiveCycles != -1 && currentCycle >= effectiveCycles)
{
if (endFilled)
{
// Additional half cycle to fill the LEDs
for (int i = 0; i < numLeds; i++)
{
leds.setPixelColor(i, animationColor);
}
leds.show();
}
else
{
turnOff();
}
stopCurrentAnimation();
}
}
lastUpdateTime = millis();
}
}
void LEDController::stopCurrentAnimation()
{
currentAnimation = None;
currentStep = 0;
currentCycle = 0;
pixelIndex = 0;
brightnessLevel = brightness;
decayStarted = false;
lastUpdateTime = millis();
}
void LEDController::printDebugInfo()
{
Serial.printf("Anim: %d, Step: %d, Cycle: %d, PixelIdx: %d, Leds numb: %d, Brightness: %d, Color: 0x%06X, Dur: %lu, Speed: %lu, Cycles: %d, EndFilled: %d\n",
currentAnimation, currentStep, currentCycle, pixelIndex, numLeds, brightness, animationColor, animationDuration, animationSpeed, animationCycles, endFilled);
}

View File

@@ -0,0 +1,511 @@
#include "Config.h"
#include "controllers/NetworkController.h"
#include <WiFi.h>
#include <WiFiClientSecure.h>
#include <HTTPClient.h>
#include <BluetoothA2DPSink.h>
#include <esp_bt.h>
NetworkController *NetworkController::instance = nullptr;
NetworkController::NetworkController()
: a2dp_sink(),
btPaired(false),
bluetoothActive(false),
bluetoothAttempted(false),
lastBluetoothtAttempt(0),
bluetoothTaskHandle(nullptr),
webhookQueue(nullptr),
webhookTaskHandle(nullptr),
provisioningMode(false)
{
instance = this;
}
void NetworkController::begin()
{
WiFiProvisionerSettings();
if (isWiFiProvisioned())
{
Serial.println("Stored WiFi credentials found. Connecting...");
wifiProvisioner.connectToWiFi();
}
// Load bluetooth paired state from nvs
preferences.begin("network", true);
btPaired = preferences.getBool("bt_paired", false);
preferences.end();
if (btPaired)
{
Serial.println("Previously paired with a device. Initializing Bluetooth.");
initializeBluetooth(); // Initialize Bluetooth if previously paired
}
else
{
Serial.println("No previous Bluetooth pairing found. Skipping Bluetooth initialization.");
}
// Load Webhook URL from NVS under the "focusdial" namespace
preferences.begin("focusdial", true);
webhookURL = preferences.getString("webhook_url", "");
preferences.end();
if (!webhookURL.isEmpty())
{
Serial.println("Loaded Webhook URL: " + webhookURL);
}
if (webhookQueue == nullptr)
{
webhookQueue = xQueueCreate(5, sizeof(char *));
}
if (webhookTaskHandle == nullptr)
{
xTaskCreatePinnedToCore(webhookTask, "Webhook Task", 4096, this, 0, &webhookTaskHandle, 1);
Serial.println("Persistent webhook task started.");
}
}
void NetworkController::update()
{
if (WiFi.status() != WL_CONNECTED)
{
WiFi.reconnect();
}
}
bool NetworkController::isWiFiProvisioned()
{
// Check for stored WiFi credentials
preferences.begin("network", true);
String storedSSID = preferences.getString("ssid", "");
preferences.end();
return !storedSSID.isEmpty(); // Return true if credentials are found
}
bool NetworkController::isWiFiConnected()
{
return (WiFi.status() == WL_CONNECTED);
}
bool NetworkController::isBluetoothPaired()
{
return btPaired;
}
void NetworkController::startProvisioning()
{
Serial.println("Starting provisioning mode...");
btPaired = false; // Reset paired state for new provisioning
bluetoothActive = true; // Enable Bluetooth for pairing
provisioningMode = true; // Indicate we are in provisioning mode
initializeBluetooth();
wifiProvisioner.setupAccessPointAndServer();
}
void NetworkController::stopProvisioning()
{
Serial.println("Stopping provisioning mode...");
bluetoothActive = false; // Disable Bluetooth after provisioning
provisioningMode = false; // Exit provisioning mode
stopBluetooth();
}
void NetworkController::reset()
{
wifiProvisioner.resetCredentials();
if (btPaired)
{
a2dp_sink.clean_last_connection();
saveBluetoothPairedState(false);
}
Serial.println("Reset complete. WiFi credentials and paired state cleared.");
}
void NetworkController::initializeBluetooth()
{
if (bluetoothTaskHandle == nullptr)
{
// Configure the A2DP sink with empty callbacks to use it for the trigger only
a2dp_sink.set_stream_reader(nullptr, false);
a2dp_sink.set_raw_stream_reader(nullptr);
a2dp_sink.set_on_volumechange(nullptr);
a2dp_sink.set_avrc_connection_state_callback(nullptr);
a2dp_sink.set_avrc_metadata_callback(nullptr);
a2dp_sink.set_avrc_rn_playstatus_callback(nullptr);
a2dp_sink.set_avrc_rn_track_change_callback(nullptr);
a2dp_sink.set_avrc_rn_play_pos_callback(nullptr);
a2dp_sink.set_spp_active(false);
a2dp_sink.set_output_active(false);
a2dp_sink.set_rssi_active(false);
a2dp_sink.set_on_connection_state_changed(btConnectionStateCallback, this);
Serial.println("Bluetooth A2DP Sink configured.");
// Create task for handling Bluetooth
xTaskCreate(bluetoothTask, "Bluetooth Task", 4096, this, 0, &bluetoothTaskHandle);
}
}
void NetworkController::startBluetooth()
{
if (btPaired)
{ // Only start if paired
bluetoothActive = true;
}
}
void NetworkController::stopBluetooth()
{
bluetoothActive = false; // Stop Bluetooth activity
}
void NetworkController::btConnectionStateCallback(esp_a2d_connection_state_t state, void *obj)
{
auto *self = static_cast<NetworkController *>(obj);
if (state == ESP_A2D_CONNECTION_STATE_CONNECTED)
{
Serial.println("Bluetooth device connected.");
// Save paired state only in provisioning mode
if (self->provisioningMode)
{
self->saveBluetoothPairedState(true);
self->btPaired = true;
Serial.println("Paired state saved during provisioning.");
}
}
else if (state == ESP_A2D_CONNECTION_STATE_DISCONNECTED)
{
Serial.println("Bluetooth device disconnected.");
// No need to set flags; task loop will handle reconnection logic based on is_connected()
}
}
void NetworkController::saveBluetoothPairedState(bool paired)
{
preferences.begin("network", false);
preferences.putBool("bt_paired", paired);
preferences.end();
btPaired = paired;
Serial.println("Bluetooth pairing state saved in NVS.");
}
void NetworkController::bluetoothTask(void *param)
{
NetworkController *self = static_cast<NetworkController *>(param);
while (true)
{
// If in provisioning mode, start Bluetooth only once
if (self->provisioningMode)
{
if (!self->bluetoothAttempted)
{
Serial.println("Starting Bluetooth for provisioning...");
self->a2dp_sink.start("Focus Dial", true);
self->bluetoothAttempted = true; // Mark as attempted to prevent repeated starts
}
}
else
{
// Normal operation mode
if (self->bluetoothActive && !self->bluetoothAttempted)
{
Serial.println("Starting Bluetooth...");
self->a2dp_sink.start("Focus Dial", true); // Auto-reconnect enabled
self->bluetoothAttempted = true;
self->lastBluetoothtAttempt = millis(); // Record the time of the start attempt
}
// If Bluetooth is active but not connected, attempt reconnect every 2 seconds
if (self->bluetoothActive && !self->a2dp_sink.is_connected() && (millis() - self->lastBluetoothtAttempt >= 2000))
{
Serial.println("Attempting Bluetooth reconnect...");
self->a2dp_sink.start("Focus Dial", true);
self->lastBluetoothtAttempt = millis(); // Update last attempt time
}
// If Bluetooth is not supposed to be active but is connected, disconnect
if (!self->bluetoothActive && self->a2dp_sink.is_connected())
{
Serial.println("Stopping Bluetooth...");
self->a2dp_sink.disconnect();
self->bluetoothAttempted = false; // Allow re-attempt later
}
}
vTaskDelay(100 / portTICK_PERIOD_MS);
}
}
void NetworkController::sendWebhookAction(const String &action)
{
if (webhookQueue == nullptr)
{
webhookQueue = xQueueCreate(5, sizeof(char *));
}
char *actionCopy = strdup(action.c_str());
if (actionCopy == nullptr)
{
Serial.println("Failed to allocate memory for webhook action.");
return;
}
if (xQueueSend(webhookQueue, &actionCopy, 0) == pdPASS)
{
Serial.println("Webhook action enqueued: " + String(actionCopy));
}
else
{
Serial.println("Failed to enqueue webhook action: Queue is full.");
free(actionCopy); // Free the memory if not enqueued
}
}
void NetworkController::webhookTask(void *param)
{
NetworkController *self = static_cast<NetworkController *>(param);
char *action;
while (true)
{
// Wait for a webhook action to arrive in the queue
if (xQueueReceive(self->webhookQueue, &action, portMAX_DELAY) == pdPASS)
{
Serial.println("Processing webhook action: " + String(action));
// Send the webhook request and check the response
bool success = self->sendWebhookRequest(String(action));
if (success)
{
Serial.println("Webhook action sent successfully.");
}
else
{
Serial.println("Failed to send webhook action.");
}
free(action); // Free the allocated memory for action
Serial.println("Finished processing webhook action.");
}
// Small delay to yield
vTaskDelay(10 / portTICK_PERIOD_MS);
}
}
bool NetworkController::sendWebhookRequest(const String &action)
{
if (webhookURL.isEmpty())
{
Serial.println("Webhook URL is not set. Cannot send action.");
return false;
}
std::unique_ptr<WiFiClient> client;
if (webhookURL.startsWith("https://"))
{
client.reset(new WiFiClientSecure());
if (!client)
{
Serial.println("Memory allocation for WiFiClientSecure failed.");
return false;
}
static_cast<WiFiClientSecure *>(client.get())->setInsecure(); // Not verifying server certificate
}
else
{
client.reset(new WiFiClient());
if (!client)
{
Serial.println("Memory allocation for WiFiClient failed.");
return false;
}
}
HTTPClient http;
bool result = false;
if (http.begin(*client, webhookURL))
{
http.addHeader("Content-Type", "application/json");
String jsonPayload = "{\"action\":\"" + action + "\"}";
// Send the POST request
int httpResponseCode = http.POST(jsonPayload);
if (httpResponseCode > 0)
{
String response = http.getString();
Serial.println("HTTP Response code: " + String(httpResponseCode));
Serial.println("Response: " + response);
result = true;
}
else
{
Serial.println("Error in sending POST: " + String(httpResponseCode));
}
http.end(); // Close the connection
}
else
{
Serial.println("Unable to connect to server.");
}
return result;
}
void NetworkController::WiFiProvisionerSettings()
{
wifiProvisioner.enableSerialDebug(true);
wifiProvisioner.AP_NAME = "Focus Dial";
wifiProvisioner.SVG_LOGO =
R"rawliteral(
<svg width="297" height="135" viewBox="0 0 99 45" xmlns="http://www.w3.org/2000/svg" style="margin:1rem auto;">
<g fill="currentColor">
<path d="m54 15h3v3h-3z"/>
<path d="m54 3h3v3h-3z"/>
<path d="m60 9v3h-6v3h-3v6h-3v-6h-3v-3h-6v-3h6v-3h3v-6h3v6h3v3z"/>
<path d="m42 3h3v3h-3z"/><path d="m42 15h3v3h-3z"/>
<path d="m21 30v12h-3v-9h-3v-3z"/><path d="m18 42v3h-6v-12h3v9z"/>
<path d="m84 33h3v12h-3z"/><path d="m48 33h3v3h6v6h-3v-3h-6z"/>
<path d="m99 42v3h-9v-15h3v12z"/><path d="m27 42h6v3h-6z"/><path d="m36 30h3v12h-3z"/>
<path d="m48 42h6v3h-6z"/><path d="m81 30h3v3h-3z"/><path d="m24 33h3v9h-3z"/><path d="m51 30h6v3h-6z"/>
<path d="m39 42h3v3h-3z"/><path d="m0 33h3v3h6v3h-6v6h-3z"/><path d="m3 30h6v3h-6z"/><path d="m72 30h3v15h-3z"/>
<path d="m42 30h3v12h-3z"/><path d="m66 33h3v9h-3z"/><path d="m78 33h3v12h-3z"/><path d="m63 42h3v3h-6v-15h6v3h-3z"/>
<path d="m27 30h6v3h-6z"/>
</g>
</svg>
<style> /* Override lib defaults */
:root {
--theme-color: #4caf50;
--font-color: #fff;
--card-background: #171717;
--black: #080808;
}
body {
background-color: var(--black);
}
input {
background-color: #2b2b2b;
}
.error input[type="text"],
.error input[type="password"] {
background-color: #3e0707;
}
input[type="text"]:disabled ,input[type="password"]:disabled ,input[type="radio"]:disabled {
color:var(--black);
}
</style>)rawliteral";
wifiProvisioner.HTML_TITLE = "Focus Dial - Provisioning";
wifiProvisioner.PROJECT_TITLE = " Focus Dial — Setup";
wifiProvisioner.PROJECT_INFO = R"rawliteral(
1. Connect to Bluetooth if you want to use the phone automation trigger.
2. Select a WiFi network to save and allow Focus Dial to trigger webhook automations.
3. Enter the webhook URL below to trigger it when a focus session starts.)rawliteral";
wifiProvisioner.FOOTER_INFO = R"rawliteral(
Focus Dial - Made by <a href="https://youtube.com/@salimbenbouz" target="_blank">Salim Benbouziyane</a>)rawliteral";
wifiProvisioner.CONNECTION_SUCCESSFUL =
"Provision Complete. Focus Dial will now start and status led will turn to blue.";
wifiProvisioner.RESET_CONFIRMATION_TEXT =
"This will erase all settings and require re-provisioning. Confirm on the device.";
wifiProvisioner.setShowInputField(true);
wifiProvisioner.INPUT_TEXT = "Webhook URL to Trigger Automation:";
wifiProvisioner.INPUT_PLACEHOLDER = "e.g., https://example.com/webhook";
wifiProvisioner.INPUT_INVALID_LENGTH = "The URL appears incomplete. Please enter the valid URL to trigger the automation.";
wifiProvisioner.INPUT_NOT_VALID = "The URL entered is not valid. Please verify it and try again.";
// Set the static methods as callbacks
wifiProvisioner.setInputCheckCallback(validateInputCallback);
wifiProvisioner.setFactoryResetCallback(factoryResetCallback);
}
// Static method for input validation callback
bool NetworkController::validateInputCallback(const String &input)
{
if (instance)
{
return instance->validateInput(input);
}
return false;
}
// Static method for factory reset callback
void NetworkController::factoryResetCallback()
{
if (instance)
{
instance->handleFactoryReset();
}
}
bool NetworkController::validateInput(const String &input)
{
String modifiedInput = input;
// Check if URL starts with "http://" or "https://"
if (!(modifiedInput.startsWith("http://") || modifiedInput.startsWith("https://")))
{
// If none supplied assume "http://"
modifiedInput = "http://" + modifiedInput;
Serial.println("Protocol missing, defaulting to http://");
}
// Basic validation
int protocolEnd = modifiedInput.indexOf("://") + 3;
int dotPosition = modifiedInput.indexOf('.', protocolEnd);
bool isValid = (dotPosition != -1);
Serial.print("Validating input: ");
Serial.println(modifiedInput);
// Save URL to NVS here if valid
if (isValid)
{
Serial.println("URL is valid. Saving to NVS...");
if (preferences.begin("focusdial", false))
{ // false means open for writing
preferences.putString("webhook_url", modifiedInput);
preferences.end();
webhookURL = modifiedInput;
Serial.println("Webhook URL saved: " + webhookURL);
}
else
{
Serial.println("Failed to open NVS for writing.");
}
}
else
{
Serial.println("Invalid URL. Not saving to NVS.");
}
return isValid;
}
void NetworkController::handleFactoryReset()
{
Serial.println("Factory reset initiated.");
reset();
}

31
firmware/src/main.cpp Normal file
View File

@@ -0,0 +1,31 @@
#include <Arduino.h>
#include "Config.h"
#include "StateMachine.h"
#include "Controllers.h"
// Global instances of controllers
DisplayController displayController(OLED_WIDTH, OLED_HEIGHT, OLED_ADDR);
LEDController ledController(LED_PIN, NUM_LEDS, LED_BRIGHTNESS);
InputController inputController(BUTTON_PIN, ENCODER_A_PIN, ENCODER_B_PIN);
NetworkController networkController;
Preferences preferences;
void setup() {
Serial.begin(115200);
// Initialize controllers
inputController.begin();
displayController.begin();
ledController.begin();
networkController.begin();
// Startup state
stateMachine.changeState(&StateMachine::startupState);
}
void loop() {
// Update state machine
stateMachine.update();
// If any animation needs to run
displayController.updateAnimation();
}

View File

@@ -0,0 +1,57 @@
#include "StateMachine.h"
#include "Controllers.h"
void AdjustState::enter()
{
Serial.println("Entering Adjust State");
lastActivity = millis();
ledController.setSolid(AMBER);
// Register state-specific handlers
inputController.onPressHandler([this]()
{
Serial.println("Adjust State: Button pressed");
StateMachine::idleState.setTimer(this->adjustDuration);
displayController.showConfirmation();
stateMachine.changeState(&StateMachine::idleState); });
inputController.onEncoderRotateHandler([this](int delta)
{
Serial.println("Adjust State: Encoder turned");
Serial.println(delta);
// Update duration with delta and enforce bounds
this->adjustDuration += (delta * 5);
if (this->adjustDuration < MIN_TIMER) {
this->adjustDuration = MIN_TIMER;
} else if (this->adjustDuration > MAX_TIMER) {
this->adjustDuration = MAX_TIMER;
}
this->lastActivity = millis(); });
}
void AdjustState::update()
{
inputController.update();
displayController.drawAdjustScreen(adjustDuration);
if (millis() - lastActivity >= (CHANGE_TIMEOUT * 1000))
{
// Transition to Idle
stateMachine.changeState(&StateMachine::idleState);
}
}
void AdjustState::exit()
{
Serial.println("Exiting Adjust State");
inputController.releaseHandlers();
}
void AdjustState::adjustTimer(int duration)
{
adjustDuration = duration;
}

View File

@@ -0,0 +1,41 @@
#include "StateMachine.h"
#include "Controllers.h"
DoneState::DoneState() : doneEnter(0) {}
void DoneState::enter()
{
Serial.println("Entering Done State");
doneEnter = millis();
ledController.setBreath(GREEN, -1, true, 2);
// Register state-specific handlers
inputController.onPressHandler([]()
{
Serial.println("Done State: Button pressed");
stateMachine.changeState(&StateMachine::idleState); });
// Send 'Stop' webhook
networkController.sendWebhookAction("stop");
}
void DoneState::update()
{
inputController.update();
ledController.update();
displayController.drawDoneScreen();
if (millis() - doneEnter >= (CHANGE_TIMEOUT * 1000))
{
// Transition to Idle after timeout
stateMachine.changeState(&StateMachine::idleState);
}
}
void DoneState::exit()
{
Serial.println("Exiting Done State");
inputController.releaseHandlers();
}

View File

@@ -0,0 +1,87 @@
#include "StateMachine.h"
#include "Controllers.h"
IdleState::IdleState() : defaultDuration(0), lastActivity(0)
{
if (nvs_flash_init() != ESP_OK)
{
Serial.println("NVS Flash Init Failed");
}
else
{
Serial.println("NVS initialized successfully.");
}
// Load the default duration
if (preferences.begin("focusdial", true))
{
defaultDuration = preferences.getInt("timer", DEFAULT_TIMER);
preferences.end();
}
}
void IdleState::enter()
{
Serial.println("Entering Idle State");
ledController.setBreath(BLUE, -1, false, 5);
// Register state-specific handlers
inputController.onPressHandler([this]()
{
Serial.println("Idle State: Button pressed");
StateMachine::timerState.setTimer(this->defaultDuration, 0);
displayController.showTimerStart();
stateMachine.changeState(&StateMachine::timerState); // Start timer
});
inputController.onLongPressHandler([this]()
{
Serial.println("Idle State: Button long pressed");
stateMachine.changeState(&StateMachine::resetState); // Transition to Reset State
});
inputController.onEncoderRotateHandler([this](int delta)
{
Serial.println("Idle State: Encoder turned");
StateMachine::adjustState.adjustTimer(this->defaultDuration);
stateMachine.changeState(&StateMachine::adjustState); // Transition to Adjust State
});
lastActivity = millis(); // Activity timer
}
void IdleState::update()
{
static unsigned long lastUpdateTime = 0;
// Controllers updates
inputController.update();
ledController.update();
networkController.update();
displayController.drawIdleScreen(defaultDuration, networkController.isWiFiConnected());
// Check if sleep timeout is reached
if (millis() - lastActivity >= (SLEEP_TIMOUT * 60 * 1000))
{
Serial.println("Idle State: Activity timeout");
stateMachine.changeState(&StateMachine::sleepState); // Transition to Sleep State
}
}
void IdleState::exit()
{
Serial.println("Exiting Idle State");
inputController.releaseHandlers();
ledController.turnOff();
}
void IdleState::setTimer(int duration)
{
defaultDuration = duration;
preferences.begin("focusdial", true);
preferences.putInt("timer", defaultDuration);
preferences.end();
}

View File

@@ -0,0 +1,71 @@
#include "StateMachine.h"
#include "Controllers.h"
PausedState::PausedState() : duration(0), elapsedTime(0), pauseEnter(0) {}
void PausedState::enter()
{
Serial.println("Entering Paused State");
pauseEnter = millis(); // Record the time when the pause started
ledController.setBreath(YELLOW, -1, false, 20);
// Register state-specific handlers
inputController.onPressHandler([this]()
{
Serial.println("Paused State: Button Pressed");
// Send 'Start' webhook (resume)
networkController.sendWebhookAction("start");
// Transition back to TimerState with the stored duration and elapsed time
StateMachine::timerState.setTimer(duration, elapsedTime);
displayController.showTimerResume();
stateMachine.changeState(&StateMachine::timerState); // Transition back to Timer State
});
inputController.onDoublePressHandler([]()
{
Serial.println("Paused State: Button Double Pressed");
// Send 'Stop' webhook (canceled)
networkController.sendWebhookAction("stop");
displayController.showCancel();
stateMachine.changeState(&StateMachine::idleState); // Transition back to Idle State
});
}
void PausedState::update()
{
inputController.update();
ledController.update();
// Redraw the paused screen with remaining time
int remainingTime = (duration * 60) - elapsedTime;
displayController.drawPausedScreen(remainingTime);
unsigned long currentTime = millis();
// Check if the pause timeout has been reached
if (currentTime - pauseEnter >= (PAUSE_TIMEOUT * 60 * 1000))
{
// Timeout reached, transition to Idle State
Serial.println("Paused State: Timout");
// Send 'Stop' webhook (timeout)
networkController.sendWebhookAction("stop");
displayController.showCancel();
stateMachine.changeState(&StateMachine::idleState); // Transition back to Idle State
}
}
void PausedState::exit()
{
Serial.println("Exiting Paused State");
inputController.releaseHandlers();
}
void PausedState::setPause(int duration, unsigned long elapsedTime)
{
this->duration = duration;
this->elapsedTime = elapsedTime;
}

View File

@@ -0,0 +1,29 @@
#include "StateMachine.h"
#include "Controllers.h"
void ProvisionState::enter()
{
Serial.println("Entering Provision State");
inputController.releaseHandlers();
displayController.drawProvisionScreen();
ledController.setSolid(AMBER);
networkController.startProvisioning();
}
void ProvisionState::update()
{
ledController.update();
if (networkController.isWiFiProvisioned() && networkController.isWiFiConnected())
{
Serial.println("Provisioning Complete, WiFi Connected");
displayController.showConnected();
networkController.stopProvisioning();
stateMachine.changeState(&StateMachine::idleState);
}
}
void ProvisionState::exit()
{
Serial.println("Exiting Provision State");
networkController.stopProvisioning();
}

View File

@@ -0,0 +1,54 @@
#include "StateMachine.h"
#include "Controllers.h"
bool resetSelected = false; // button selection
void ResetState::enter()
{
Serial.println("Entering Reset State");
ledController.setBreath(MAGENTA, -1, false, 10);
// Register state-specific handlers
inputController.onEncoderRotateHandler([this](int delta)
{
if (delta > 0) {
resetSelected = true; // Select "RESET"
} else if (delta < 0) {
resetSelected = false; // Select "CANCEL"
} });
inputController.onPressHandler([this]()
{
if (resetSelected) {
Serial.println("Reset State: RESET button pressed, rebooting.");
displayController.showReset();
networkController.reset();
resetStartTime = millis();
} else {
Serial.println("Reset State: CANCEL button pressed, returning to Idle.");
displayController.showCancel();
stateMachine.changeState(&StateMachine::idleState);
} });
}
void ResetState::update()
{
inputController.update();
ledController.update();
displayController.drawResetScreen(resetSelected);
if (resetStartTime > 0 && (millis() - resetStartTime >= 1000))
{
Serial.println("Restarting ...");
ESP.restart(); // Restart after 1 second
}
}
void ResetState::exit()
{
Serial.println("Exiting Reset State");
inputController.releaseHandlers();
ledController.turnOff();
}

View File

@@ -0,0 +1,37 @@
#include "StateMachine.h"
#include "Controllers.h"
void SleepState::enter()
{
Serial.println("Entering Sleep State");
ledController.turnOff();
displayController.clear();
// Register state-specific handlers
inputController.onPressHandler([]()
{
Serial.println("Sleep State: Button pressed");
stateMachine.changeState(&StateMachine::idleState); });
inputController.onLongPressHandler([]()
{
Serial.println("Sleep State: long pressed");
stateMachine.changeState(&StateMachine::idleState); });
inputController.onEncoderRotateHandler([this](int delta)
{
Serial.println("Sleep State: Encoder turned");
stateMachine.changeState(&StateMachine::idleState); });
}
void SleepState::update()
{
inputController.update();
}
void SleepState::exit()
{
Serial.println("Exiting Sleep State");
inputController.releaseHandlers();
}

View File

@@ -0,0 +1,37 @@
#include "StateMachine.h"
#include "Controllers.h"
StartupState::StartupState() : startEnter(0) {}
void StartupState::enter()
{
Serial.println("Entering Splash State");
displayController.drawSplashScreen();
ledController.setSpinner(TEAL, -1);
startEnter = millis();
}
void StartupState::update()
{
ledController.update();
if (millis() - startEnter >= (SPLASH_DURATION * 1000))
{
if (networkController.isWiFiProvisioned())
{
stateMachine.changeState(&StateMachine::idleState); // Transition to Idle
}
else
{
stateMachine.changeState(&StateMachine::provisionState); // Trigger Provision
}
}
}
void StartupState::exit()
{
ledController.turnOff();
Serial.println("Exiting Splash State");
}

View File

@@ -0,0 +1,77 @@
#include "StateMachine.h"
#include "Controllers.h"
TimerState::TimerState() : duration(0), elapsedTime(0), startTime(0) {}
void TimerState::enter()
{
Serial.println("Entering Timer State");
// Start time based on the elapsed time
startTime = millis() - (elapsedTime * 1000);
displayController.drawTimerScreen(duration * 60);
ledController.startFillAndDecay(RED, ((duration * 60) - elapsedTime) * 1000);
// Register state-specific handlers
inputController.onPressHandler([this]()
{
Serial.println("Timer State: Button Pressed");
// Send 'Stop' webhook (pause)
networkController.sendWebhookAction("stop");
displayController.showTimerPause();
// Transition to PausedState and set elapsed time
StateMachine::pausedState.setPause(this->duration, this->elapsedTime); // Save current elapsed time
stateMachine.changeState(&StateMachine::pausedState); // Transition to Paused State
});
inputController.onDoublePressHandler([this]()
{
Serial.println("Timer State: Button Double Pressed");
// Send 'Stop' webhook (canceled)
networkController.sendWebhookAction("stop");
displayController.showCancel();
stateMachine.changeState(&StateMachine::idleState); // Transition to IdleState
});
networkController.startBluetooth();
networkController.sendWebhookAction("start");
}
void TimerState::update()
{
inputController.update();
ledController.update();
unsigned long currentTime = millis();
elapsedTime = (currentTime - startTime) / 1000;
int remainingSeconds = duration * 60 - elapsedTime;
displayController.drawTimerScreen(remainingSeconds);
// Check if the timer is done
if (remainingSeconds <= 0)
{
Serial.println("Timer State: Done");
displayController.showTimerDone();
stateMachine.changeState(&StateMachine::doneState); // Transition to Done State
}
}
void TimerState::exit()
{
inputController.releaseHandlers();
networkController.stopBluetooth();
ledController.turnOff();
Serial.println("Exiting Timer State");
}
void TimerState::setTimer(int duration, unsigned long elapsedTime)
{
this->duration = duration;
this->elapsedTime = elapsedTime;
}