#include <Arduino.h>
#include <U8x8lib.h>
#include <EncButton.h>
#include <FastLED.h>
const byte encAPin = 3;
const byte encBPin = 2;
const byte encBtnPin = 4;
const byte led = 8;
enum Action {
NONE, CLICK, UP, DOWN
};
class Counter {
private:
int value;
int delta;
int _min;
int _max;
public:
Counter(int value, int delta, int minimum, int maximum) {
this->value = value;
this->delta = delta;
this->_min = minimum;
this->_max = maximum;
}
int get() {
return this->value;
}
void inc() {
this->value += delta;
if (this->value > this->_max)
this->value = this->_max;
}
void dec() {
this->value -= delta;
if (this->value < this->_min)
this->value = this->_min;
}
int getMax() {
return this->_max;
}
int getMin() {
return this->_min;
}
bool isMax() {
return this->value == this->_max;
}
bool isMin() {
return this->value == this->_min;
}
};
class Menu {
private:
int activeOptionIdx;
char** options;
int optionsCnt;
bool enabled;
public:
Menu(int activeOptionIdx, char** options, int optionsCnt, bool enabled) {
this->activeOptionIdx = activeOptionIdx;
this->options = options;
this->optionsCnt = optionsCnt;
this->enabled = enabled;
}
int getActiveOptionIdx() {
return this->activeOptionIdx;
}
/**
* Returns true when active index of menu is equivalent to `idx`.
*/
bool isActiveOption(int idx) {
return idx == this->activeOptionIdx;
}
char* getOption(int idx) {
return this->options[idx];
}
char* getPrevOption() {
if (this->activeOptionIdx == 0)
return this->options[this->optionsCnt - 1];
return this->options[this->activeOptionIdx - 1];
}
char* getActiveOption() {
return this->options[this->activeOptionIdx];
}
char* getNextOption() {
if (this->activeOptionIdx == this->optionsCnt - 1)
return this->options[0];
return this->options[this->activeOptionIdx + 1];
}
int getOptionsCnt() {
return this->optionsCnt;
}
void enable() {
this->enabled = true;
}
void disable() {
this->enabled = false;
}
bool isEnabled() {
return this->enabled;
}
bool isDisabled() {
return !this->enabled;
}
/**
* Returns true when option with index `idx` was active when menu was disabled
*/
bool isChosenOption(int idx) {
return this->isDisabled() && this->isActiveOption(idx);
}
void next() {
this->activeOptionIdx++;
if (this->activeOptionIdx >= optionsCnt)
this->activeOptionIdx = 0;
}
void prev() {
this->activeOptionIdx--;
if (this->activeOptionIdx < 0)
this->activeOptionIdx = this->optionsCnt - 1;
}
};
typedef struct State {
Menu mainMenu;
Menu colorsMenu;
Counter colors[3];
Menu brightnessMenu;
Counter brightness;
Menu modesMenu;
} State;
enum MainMenuOption { COLOR, BRIGHTNESS, MODES };
const int MAIN_MENU_SZ = 3;
const char* mainMenuStrs[MAIN_MENU_SZ] = {
"COLOR", "BRIGHTNESS", "MODES"
};
enum ColorsMenuOption { RED, GREEN, BLUE, BACK };
const int COLORS_MENU_SZ = 4;
const char* colorsMenuStrs[COLORS_MENU_SZ] = {
"RED", "GREEN", "BLUE", "BACK"
};
enum BrightnessMenuOption { STATIC_BRIGHTNESS, BREATH };
const int BRIGHTNESS_MENU_SZ = 2;
const char* brightnessMenuStrs[BRIGHTNESS_MENU_SZ] = {
"STATIC", "BREATH"
};
enum ModesMenuOption { STATIC_, WAVE, RAINBOW, RANDOM };
const int MODES_MENU_SZ = 4;
const char* modesMenuStrs[MODES_MENU_SZ] = {
"STATIC", "WAVE", "RAINBOW", "RANDOM"
};
State state = {
Menu(COLOR, mainMenuStrs, MAIN_MENU_SZ, true),
Menu(GREEN, colorsMenuStrs, COLORS_MENU_SZ, false),
{
Counter(127, 16, 0, 255),
Counter(127, 16, 0, 255),
Counter(127, 16, 0, 255)
},
Menu(STATIC_BRIGHTNESS, brightnessMenuStrs, BRIGHTNESS_MENU_SZ, false),
Counter(255, 16, 0, 255),
Menu(STATIC_, modesMenuStrs, MODES_MENU_SZ, false)
};
EncButton enc(encAPin, encBPin, encBtnPin);
U8X8_SSD1306_128X64_NONAME_HW_I2C u8x8;
#define NUM_LEDS 8
CRGB leds[NUM_LEDS];
void isr() {
enc.tickISR();
}
void setup() {
attachInterrupt(0, isr, CHANGE);
attachInterrupt(1, isr, CHANGE);
enc.setEncISR(true);
u8x8.begin();
u8x8.setFont(u8x8_font_amstrad_cpc_extended_r);
rewriteDisplay();
FastLED.addLeds<NEOPIXEL, led>(leds, NUM_LEDS);
updateStaticColor();
}
Action registerAction() {
enc.tick();
if (enc.turn()) {
if (enc.dir() > 0)
return UP;
else
return DOWN;
}
if (enc.click())
return CLICK;
return NONE;
}
bool changeState(Action action) {
switch (action) {
case UP:
if (state.mainMenu.isEnabled())
state.mainMenu.next();
if (state.mainMenu.isChosenOption(BRIGHTNESS)) {
if (state.brightnessMenu.isEnabled())
state.brightnessMenu.next();
else if (state.brightnessMenu.isChosenOption(STATIC_BRIGHTNESS))
state.brightness.inc();
}
if (state.mainMenu.isChosenOption(COLOR)) {
if (state.colorsMenu.isEnabled())
state.colorsMenu.next();
else
state.colors[state.colorsMenu.getActiveOptionIdx()].inc();
}
if (state.mainMenu.isChosenOption(MODES)) {
if (state.modesMenu.isEnabled())
state.modesMenu.next();
}
return true;
case DOWN:
if (state.mainMenu.isEnabled())
state.mainMenu.prev();
if (state.mainMenu.isChosenOption(BRIGHTNESS)) {
if (state.brightnessMenu.isEnabled())
state.brightnessMenu.prev();
else if (state.brightnessMenu.isChosenOption(STATIC_BRIGHTNESS))
state.brightness.dec();
}
if (state.mainMenu.isChosenOption(COLOR)) {
if (state.colorsMenu.isEnabled())
state.colorsMenu.prev();
else
state.colors[state.colorsMenu.getActiveOptionIdx()].dec();
}
if (state.mainMenu.isChosenOption(MODES)) {
if (state.modesMenu.isEnabled())
state.modesMenu.prev();
}
return true;
case CLICK:
if (state.mainMenu.isEnabled()) {
state.mainMenu.disable();
if (state.mainMenu.isChosenOption(COLOR))
state.colorsMenu.enable();
if (state.mainMenu.isChosenOption(BRIGHTNESS))
state.brightnessMenu.enable();
if (state.mainMenu.isChosenOption(MODES))
state.modesMenu.enable();
}
else if (state.mainMenu.isChosenOption(BRIGHTNESS)) {
if (state.brightnessMenu.isEnabled() && state.brightnessMenu.isActiveOption(STATIC_BRIGHTNESS))
state.brightnessMenu.disable();
else {
state.brightnessMenu.disable();
state.mainMenu.enable();
}
}
else if (state.mainMenu.isChosenOption(COLOR)) {
if (state.colorsMenu.isDisabled())
state.colorsMenu.enable();
else if (state.colorsMenu.isActiveOption(BACK)) {
state.colorsMenu.disable();
state.mainMenu.enable();
}
else
state.colorsMenu.disable();
}
else if (state.mainMenu.isChosenOption(MODES)) {
state.modesMenu.disable();
state.mainMenu.enable();
}
return true;
case NONE:
break;
default:
// Serial.println("Error");
break;
}
return false;
}
uint32_t getColor() {
return (uint32_t)state.colors[0].get() * 256 * 256
+ (uint32_t)state.colors[1].get() * 256
+ (uint32_t)state.colors[2].get();
}
bool breathDir = true;
void updateBrithness() {
if (state.brightnessMenu.isActiveOption(BREATH)) {
if (state.brightness.isMax())
breathDir = false;
if (state.brightness.isMin())
breathDir = true;
if (breathDir)
state.brightness.inc();
else
state.brightness.dec();
}
FastLED.setBrightness(state.brightness.get());
}
void updateStaticColor() {
for (int i = 0; i < NUM_LEDS; ++i)
leds[i].setColorCode(getColor());
}
int ledIndex = 0;
void updateWaveColor() {
ledIndex++;
ledIndex %= NUM_LEDS;
for (int i = 0; i < NUM_LEDS; ++i)
leds[i].setColorCode(i == ledIndex ? getColor() : CRGB::Black);
}
const CRGB rainbowLeds[8] {
CRGB::IndianRed, CRGB::Coral, CRGB::Yellow,
CRGB::SeaGreen, CRGB::DodgerBlue, CRGB::CRGB::MediumBlue,
CRGB::MediumPurple, CRGB::GhostWhite
};
void updateRainbowColor() {
ledIndex++;
ledIndex %= NUM_LEDS;
for (int i = 0; i < NUM_LEDS; ++i)
leds[i] = rainbowLeds[(i + ledIndex) % NUM_LEDS];
}
void updateRandomColor() {
for (int i = 0; i < NUM_LEDS; ++i)
leds[i].setColorCode(
(uint32_t) random(256) * 256 * 256
+ (uint32_t) random(256) * 256
+ (uint32_t) random(256)
);
}
int lastTimeUpdate = 0;
int delta = 100;
void updateLED() {
if ((int)millis() - lastTimeUpdate < delta)
return;
lastTimeUpdate = (int)millis();
updateBrithness();
if (state.modesMenu.isActiveOption(STATIC_))
updateStaticColor();
if (state.modesMenu.isActiveOption(WAVE))
updateWaveColor();
if (state.modesMenu.isActiveOption(RAINBOW))
updateRainbowColor();
if (state.modesMenu.isActiveOption(RANDOM))
updateRandomColor();
FastLED.show();
}
void addSimpleMenuOnDisplay(Menu menu, int startX=3, int startY=1) {
char strTmp[16] = {};
for (int i = 0; i < menu.getOptionsCnt(); ++i) {
sprintf(
strTmp, "%s%s",
menu.isActiveOption(i) ? "> " : "",
menu.getOption(i)
);
u8x8.drawString(startX, startY + (i * 2), strTmp);
}
}
void addRingMenuOnDisplay(Menu menu, int startX=3, int startY=1) {
char currOption[16] = {};
sprintf(
currOption, "> %s", menu.getActiveOption()
);
u8x8.drawString(startX, startY, menu.getPrevOption());
u8x8.drawString(startX, startY + 2, currOption);
u8x8.drawString(startX, startY + 4, menu.getNextOption());
}
void addProgressBarOnDisplay(Counter counter, char* label, int barSz=16, int startY=2) {
int value = (counter.get() + 1) / barSz;
int length = counter.getMax() / barSz + 1;
char strTmp[length+1] = {};
for (int i = 1; i <= length; ++i)
strTmp[i-1] = i <= value ? '=' : ' ';
u8x8.drawString(1, startY, label);
u8x8.drawString(0, startY+2, strTmp);
}
static int MENU_OPTIONS_CNT_FOR_RING = 4;
void smartMenuAdding(Menu menu) {
if (menu.getOptionsCnt() >= MENU_OPTIONS_CNT_FOR_RING)
addRingMenuOnDisplay(menu);
else
addSimpleMenuOnDisplay(menu);
}
void rewriteDisplay() {
u8x8.clearDisplay();
char strTmp[16] = {};
if (state.mainMenu.isEnabled())
smartMenuAdding(state.mainMenu);
else if (state.mainMenu.isChosenOption(COLOR)) {
if (state.colorsMenu.isEnabled())
smartMenuAdding(state.colorsMenu);
else
addProgressBarOnDisplay(
state.colors[state.colorsMenu.getActiveOptionIdx()],
state.colorsMenu.getActiveOption()
);
}
else if (state.mainMenu.isChosenOption(BRIGHTNESS)) {
if (state.brightnessMenu.isEnabled())
smartMenuAdding(state.brightnessMenu);
else
addProgressBarOnDisplay(state.brightness, state.mainMenu.getActiveOption());
}
else if (state.mainMenu.isChosenOption(MODES)) {
if (state.modesMenu.isEnabled())
smartMenuAdding(state.modesMenu);
}
}
void loop() {
Action action = registerAction();
if (changeState(action)) {
rewriteDisplay();
}
updateLED();
}