#include <Arduino.h>
#include <Adafruit_GFX.h>
#include <Adafruit_ILI9341.h>
constexpr int ROWS = 128;
constexpr int COLS = 5;
constexpr int PARAMS = 4;
constexpr int CELL_W = 40;
constexpr int CELL_H = 20;
constexpr int CELL_SPACING_X = 45;
constexpr int CELL_SPACING_Y = 25;
constexpr int CELL_START_X = 40;
constexpr int ROWS_PER_PAGE = 8;
#define TFT_CS 14
#define TFT_RST 12
#define TFT_DC 13
#define TFT_MOSI 23
#define TFT_CLK 18
#define TFT_MISO 19
Adafruit_ILI9341 tft = Adafruit_ILI9341(TFT_CS, TFT_DC, TFT_RST);
#define ENC1_CLK 32
#define ENC1_DT 33
#define ENC1_SW 25
#define ENC2_CLK 26
#define ENC2_DT 27
#define ENC2_SW 21
#define POT_PIN 36
enum SystemState { STATE_IDLE, STATE_EDIT, STATE_PLAY };
volatile SystemState currentState = STATE_IDLE;
volatile SystemState prevState = STATE_IDLE;
uint8_t grid[ROWS][COLS][PARAMS];
int cursorRow = 0;
int cursorCol = 0;
int lastCLK1 = HIGH;
int lastCLK2 = HIGH;
int lastEnc2SW = HIGH;
int menuPos = 0;
int currentRow = 0;
int stepCol = 0;
unsigned long lastEditBtnMs = 0;
const unsigned long DEBOUNCE_MS = 250;
volatile uint16_t tempoMs = 120;
void drawGrid();
void drawMenu();
void drawStatusIndicator();
void drawTempoBox();
void playCell(int row, int col);
const char* stateName(SystemState s) {
switch (s) {
case STATE_IDLE: return "IDLE";
case STATE_EDIT: return "EDIT";
case STATE_PLAY: return "PLAY";
default: return "UNKNOWN";
}
}
void changeState(SystemState newState) {
if (newState != currentState) {
prevState = currentState;
currentState = newState;
Serial.print("Cambio de estado: ");
Serial.print(stateName(prevState));
Serial.print(" -> ");
Serial.println(stateName(currentState));
}
}
TaskHandle_t hTaskPlay = nullptr;
TaskHandle_t hTaskUI = nullptr;
void taskPlay(void *){
unsigned long lastStepMs = 0;
for(;;){
if (currentState == STATE_PLAY) {
unsigned long now = millis();
uint16_t localTempo = tempoMs;
if (now - lastStepMs >= localTempo) {
playCell(currentRow, stepCol);
stepCol++;
if (stepCol >= COLS) {
stepCol = 0;
currentRow++;
if (currentRow >= ROWS) {
currentRow = 0;
changeState(STATE_IDLE);
}
}
lastStepMs = now;
}
}
vTaskDelay(1);
}
}
void taskUI(void *){
uint16_t lastTempoLocal = 0xFFFF;
SystemState lastStateShown = (SystemState)255;
for(;;){
if (tempoMs != lastTempoLocal) {
drawTempoBox();
lastTempoLocal = tempoMs;
}
if (currentState != lastStateShown) {
drawStatusIndicator();
lastStateShown = currentState;
}
vTaskDelay(pdMS_TO_TICKS(80));
}
}
void setup() {
Serial.begin(115200);
delay(50);
tft.begin();
tft.setRotation(1);
tft.fillScreen(ILI9341_BLACK);
tft.setTextSize(2);
pinMode(ENC1_CLK, INPUT_PULLUP);
pinMode(ENC1_DT, INPUT_PULLUP);
pinMode(ENC1_SW, INPUT_PULLUP);
pinMode(ENC2_CLK, INPUT_PULLUP);
pinMode(ENC2_DT, INPUT_PULLUP);
pinMode(ENC2_SW, INPUT_PULLUP);
analogReadResolution(12);
for (int r = 0; r < ROWS; r++)
for (int c = 0; c < COLS; c++)
for (int v = 0; v < PARAMS; v++)
grid[r][c][v] = (v * 0x11);
drawGrid();
xTaskCreatePinnedToCore(taskPlay, "taskPlay", 4096, nullptr, 1, &hTaskPlay, 1);
xTaskCreatePinnedToCore(taskUI, "taskUI", 4096, nullptr, 1, &hTaskUI, 0);
}
void loop() {
int pot = analogRead(POT_PIN);
tempoMs = map(pot, 0, 4095, 50, 600);
int currentCLK1 = digitalRead(ENC1_CLK);
if (currentCLK1 != lastCLK1 && currentCLK1 == LOW) {
if (currentState != STATE_EDIT) {
if (digitalRead(ENC1_DT) != currentCLK1) cursorCol++;
else cursorCol--;
if (cursorCol < 0) cursorCol = 0;
if (cursorCol > COLS - 1) cursorCol = COLS - 1;
drawGrid();
} else {
if (digitalRead(ENC1_DT) != currentCLK1) menuPos++;
else menuPos--;
if (menuPos < 0) menuPos = 0;
if (menuPos > PARAMS - 1) menuPos = PARAMS - 1;
drawMenu();
}
}
lastCLK1 = currentCLK1;
int currentCLK2 = digitalRead(ENC2_CLK);
if (currentCLK2 != lastCLK2 && currentCLK2 == LOW) {
if (currentState != STATE_EDIT) {
if (digitalRead(ENC2_DT) != currentCLK2) cursorRow++;
else cursorRow--;
if (cursorRow < 0) cursorRow = 0;
if (cursorRow > ROWS - 1) cursorRow = ROWS - 1;
drawGrid();
} else {
if (digitalRead(ENC2_DT) != currentCLK2) grid[cursorRow][cursorCol][menuPos]++;
else grid[cursorRow][cursorCol][menuPos]--;
drawMenu();
}
}
lastCLK2 = currentCLK2;
bool enc1Pressed = (digitalRead(ENC1_SW) == LOW);
if (enc1Pressed && (millis() - lastEditBtnMs > DEBOUNCE_MS)) {
if (currentState == STATE_EDIT) {
changeState(STATE_IDLE);
drawGrid();
} else if (currentState == STATE_IDLE) {
changeState(STATE_EDIT);
drawMenu();
}
lastEditBtnMs = millis();
}
bool currentEnc2SW = digitalRead(ENC2_SW);
if (lastEnc2SW == HIGH && currentEnc2SW == LOW) {
if (currentState == STATE_EDIT) {
playCell(cursorRow, cursorCol);
} else if (currentState == STATE_IDLE) {
currentRow = 0;
stepCol = 0;
changeState(STATE_PLAY);
} else if (currentState == STATE_PLAY) {
changeState(STATE_IDLE);
}
}
lastEnc2SW = currentEnc2SW;
}
void drawGrid() {
tft.fillScreen(ILI9341_BLACK);
int scrollOffset = (cursorRow / ROWS_PER_PAGE) * ROWS_PER_PAGE;
for (int r = 0; r < ROWS_PER_PAGE; r++) {
int rowIndex = scrollOffset + r;
if (rowIndex >= ROWS) break;
tft.setTextColor(ILI9341_CYAN, ILI9341_BLACK);
tft.setCursor(5, 20 + r * CELL_SPACING_Y);
tft.print(rowIndex);
for (int c = 0; c < COLS; c++) {
int x = CELL_START_X + c * CELL_SPACING_X;
int y = 20 + r * CELL_SPACING_Y;
if (rowIndex == cursorRow && c == cursorCol) {
tft.fillRect(x - 5, y - 5, CELL_W, CELL_H, ILI9341_YELLOW);
tft.setTextColor(ILI9341_BLACK, ILI9341_YELLOW);
} else {
tft.setTextColor(ILI9341_WHITE, ILI9341_BLACK);
}
tft.setCursor(x, y);
char buf[3];
sprintf(buf, "%02X", grid[rowIndex][c][0]);
tft.print(buf);
}
}
if (currentState == STATE_EDIT) drawMenu();
drawStatusIndicator();
drawTempoBox();
}
void drawMenu() {
int yBase = 240 - 40;
tft.fillRect(0, yBase, 240, 40, ILI9341_BLACK);
for (int i = 0; i < PARAMS; i++) {
int x = 20 + i * 50;
if (currentState == STATE_EDIT && i == menuPos) {
tft.fillRect(x - 5, yBase, 40, 20, ILI9341_BLUE);
tft.setTextColor(ILI9341_WHITE, ILI9341_BLUE);
} else {
tft.setTextColor(ILI9341_WHITE, ILI9341_BLACK);
}
tft.setCursor(x, yBase + 5);
char buf[3];
sprintf(buf, "%02X", grid[cursorRow][cursorCol][i]);
tft.print(buf);
}
}
void drawStatusIndicator() {
uint16_t color = ILI9341_WHITE;
switch (currentState) {
case STATE_IDLE: color = ILI9341_RED; break;
case STATE_EDIT: color = ILI9341_GREEN; break;
case STATE_PLAY: color = ILI9341_BLUE; break;
}
int x = tft.width() - 12;
int y = 2;
tft.fillRect(x, y, 10, 10, color);
}
void drawTempoBox() {
char buf[20];
snprintf(buf, sizeof(buf), "%dms", (int)tempoMs);
int16_t x1, y1;
uint16_t w, h;
tft.getTextBounds(buf, 0, 0, &x1, &y1, &w, &h);
const int pad = 6;
const int boxW = w + pad * 2;
const int boxH = h + pad * 2;
const int bx = tft.width() - boxW;
const int by = (tft.height() - boxH) / 2;
tft.fillRect(bx, by, boxW, boxH, ILI9341_BLACK);
tft.setCursor(bx + pad, by + pad);
tft.setTextColor(ILI9341_CYAN, ILI9341_BLACK);
tft.print(buf);
}
void playCell(int row, int col) {
tft.fillRect(60, 200, 160, 30, ILI9341_RED);
tft.setCursor(70, 210);
tft.setTextColor(ILI9341_WHITE, ILI9341_RED);
tft.print(" PLAY r=");
tft.print(row);
vTaskDelay(pdMS_TO_TICKS(100));
drawGrid();
}