Enclosure files + firmware
This commit is contained in:
82
firmware/src/Animation.cpp
Normal file
82
firmware/src/Animation.cpp
Normal 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;
|
||||
}
|
||||
47
firmware/src/StateMachine.cpp
Normal file
47
firmware/src/StateMachine.cpp
Normal 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
|
||||
}
|
||||
}
|
||||
458
firmware/src/controllers/DisplayController.cpp
Normal file
458
firmware/src/controllers/DisplayController.cpp
Normal 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);
|
||||
}
|
||||
141
firmware/src/controllers/InputController.cpp
Normal file
141
firmware/src/controllers/InputController.cpp
Normal 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
|
||||
}
|
||||
}
|
||||
241
firmware/src/controllers/LedController.cpp
Normal file
241
firmware/src/controllers/LedController.cpp
Normal 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);
|
||||
}
|
||||
511
firmware/src/controllers/NetworkController.cpp
Normal file
511
firmware/src/controllers/NetworkController.cpp
Normal 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
31
firmware/src/main.cpp
Normal 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();
|
||||
}
|
||||
57
firmware/src/states/AdjustState.cpp
Normal file
57
firmware/src/states/AdjustState.cpp
Normal 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;
|
||||
}
|
||||
41
firmware/src/states/DoneState.cpp
Normal file
41
firmware/src/states/DoneState.cpp
Normal 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();
|
||||
}
|
||||
87
firmware/src/states/IdleState.cpp
Normal file
87
firmware/src/states/IdleState.cpp
Normal 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();
|
||||
}
|
||||
71
firmware/src/states/PausedState.cpp
Normal file
71
firmware/src/states/PausedState.cpp
Normal 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;
|
||||
}
|
||||
29
firmware/src/states/ProvisionState.cpp
Normal file
29
firmware/src/states/ProvisionState.cpp
Normal 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();
|
||||
}
|
||||
54
firmware/src/states/ResetState.cpp
Normal file
54
firmware/src/states/ResetState.cpp
Normal 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();
|
||||
}
|
||||
37
firmware/src/states/SleepState.cpp
Normal file
37
firmware/src/states/SleepState.cpp
Normal 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();
|
||||
}
|
||||
37
firmware/src/states/StartupState.cpp
Normal file
37
firmware/src/states/StartupState.cpp
Normal 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");
|
||||
}
|
||||
77
firmware/src/states/TimerState.cpp
Normal file
77
firmware/src/states/TimerState.cpp
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user