Enclosure files + firmware
This commit is contained in:
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();
|
||||
}
|
||||
Reference in New Issue
Block a user