#include <SPI.h>
#include <Adafruit_GFX.h>
#include <Adafruit_ILI9341.h>
// ==================================================
// Feature-Schalter
// ==================================================
#define ENABLE_STATUS_BOX 0
#define ENABLE_ENCODER_ORDER 1
#define ENABLE_LEARN_LONG_ORDER 0
// ==================================================
// TFT
// ==================================================
#define TFT_CS D3
#define TFT_DC D2
Adafruit_ILI9341 tft(TFT_CS, TFT_DC);
// ==================================================
// Pinbelegung
// ==================================================
// Loop 1
#define LOOP1_LED_R PB6
#define LOOP1_LED_G D5
#define LOOP1_LED_B D6
#define LOOP1_SW A3
#define LOOP1_TIP A0
#define LOOP1_RING D7
// Loop 2
#define LOOP2_LED_R A5
#define LOOP2_LED_G D14
#define LOOP2_LED_B D8
#define LOOP2_SW D9
#define LOOP2_TIP D10
#define LOOP2_RING D4
// Loop 3
#define LOOP3_LED_R PB8
#define LOOP3_LED_G PD0
#define LOOP3_LED_B PD2
#define LOOP3_SW PC6
#define LOOP3_TIP PA11
#define LOOP3_RING PA8
// Learn
#define LEARN_SW A2
// Rotary Encoder
#define ENC_A PB11
#define ENC_B PB12
#define ENC_SW PB13
// ==================================================
// Einstellungen
// ==================================================
const uint8_t NUM_LOOPS = 3;
const uint8_t PC_COUNT = 128;
const unsigned long DEBOUNCE_MS = 40;
const unsigned long TIP_PULSE_MS = 120;
const unsigned long RING_WAIT_MS = 300;
#define LED_ON HIGH
#define LED_OFF LOW
#define SWITCH_PRESSED LOW
#define RING_0V LOW
#define RING_3V3 HIGH
// ==================================================
// Zustaende
// ==================================================
enum LoopMode : uint8_t {
MODE_OFF,
MODE_GREEN,
MODE_YELLOW
};
enum TipState : uint8_t {
TIP_IDLE,
TIP_PULSE_ACTIVE,
TIP_WAIT_ON,
TIP_WAIT_OFF
};
enum SystemMode : uint8_t {
SYS_PLAY,
SYS_LEARN
};
struct LoopChannel {
uint32_t ledR;
uint32_t ledG;
uint32_t ledB;
uint32_t sw;
uint32_t tip;
uint32_t ring;
LoopMode mode;
TipState tipState;
unsigned long tipTimer;
bool swLastRaw;
bool swStable;
unsigned long swLastChange;
bool lastRing;
};
// ==================================================
// Loop Array
// ==================================================
LoopChannel loops[NUM_LOOPS] = {
{
LOOP1_LED_R, LOOP1_LED_G, LOOP1_LED_B,
LOOP1_SW, LOOP1_TIP, LOOP1_RING,
MODE_OFF, TIP_IDLE, 0,
HIGH, HIGH, 0,
HIGH
},
{
LOOP2_LED_R, LOOP2_LED_G, LOOP2_LED_B,
LOOP2_SW, LOOP2_TIP, LOOP2_RING,
MODE_OFF, TIP_IDLE, 0,
HIGH, HIGH, 0,
HIGH
},
{
LOOP3_LED_R, LOOP3_LED_G, LOOP3_LED_B,
LOOP3_SW, LOOP3_TIP, LOOP3_RING,
MODE_OFF, TIP_IDLE, 0,
HIGH, HIGH, 0,
HIGH
}
};
const char* loopLabels[NUM_LOOPS] = {
"L1",
"L2",
"L3"
};
// ==================================================
// Systemvariablen
// ==================================================
bool screenReady = false;
SystemMode systemMode = SYS_PLAY;
bool screenUpdatesSuspended = false;
bool pendingFullRedraw = false;
bool pcRecallInProgress = false;
// Presets
bool presetValid[PC_COUNT];
uint8_t presetMode[PC_COUNT][NUM_LOOPS];
uint8_t presetOrder[PC_COUNT][NUM_LOOPS];
uint8_t presetParallel[PC_COUNT][NUM_LOOPS];
// Learn Snapshot
uint8_t learnMode[NUM_LOOPS];
uint8_t learnOrder[NUM_LOOPS];
uint8_t learnParallel[NUM_LOOPS];
// Reihenfolge
uint8_t loopOrder[NUM_LOOPS] = {0, 1, 2};
// loopParallel[id] == true bedeutet:
// Loop id liegt parallel unter dem vorherigen Loop in loopOrder[]
bool loopParallel[NUM_LOOPS] = {false, false, false};
// Learn Button
bool learnLastRaw = HIGH;
bool learnStable = HIGH;
unsigned long learnLastChange = 0;
bool learnReady = false;
// Rotary Encoder
bool encLastA = HIGH;
bool encLastButtonRaw = HIGH;
bool encButtonStable = HIGH;
unsigned long encButtonLastChange = 0;
uint8_t encoderSelectedPos = 0;
bool encoderGrabbed = false;
// Serial PC Input
char serialBuf[8];
uint8_t serialLen = 0;
// ==================================================
// LED
// ==================================================
void setLed(uint8_t id) {
LoopChannel &l = loops[id];
if (l.mode == MODE_OFF) {
digitalWrite(l.ledR, LED_OFF);
digitalWrite(l.ledG, LED_OFF);
digitalWrite(l.ledB, LED_OFF);
} else if (l.mode == MODE_GREEN) {
digitalWrite(l.ledR, LED_OFF);
digitalWrite(l.ledG, LED_ON);
digitalWrite(l.ledB, LED_OFF);
} else {
digitalWrite(l.ledR, LED_ON);
digitalWrite(l.ledG, LED_ON);
digitalWrite(l.ledB, LED_OFF);
}
}
// ==================================================
// TFT
// ==================================================
const int BOX_W = 48;
const int BOX_H = 42;
const int TOP_Y = 99;
const int BOT_Y = 145;
const int TOP_MID_Y = TOP_Y + BOX_H / 2; // 120
const int BOT_MID_Y = BOT_Y + BOX_H / 2; // 166
uint16_t boxColor(LoopMode m) {
if (m == MODE_GREEN) return ILI9341_GREEN;
if (m == MODE_YELLOW) return ILI9341_YELLOW;
return ILI9341_BLACK;
}
uint16_t textColor(LoopMode m) {
if (m == MODE_OFF) return ILI9341_WHITE;
return ILI9341_BLACK;
}
void drawArrow(int x1, int y, int x2) {
tft.drawLine(x1, y, x2, y, ILI9341_WHITE);
tft.fillTriangle(x2, y, x2 - 5, y - 4, x2 - 5, y + 4, ILI9341_WHITE);
}
uint8_t getPositionForLoopId(uint8_t id) {
for (uint8_t pos = 0; pos < NUM_LOOPS; pos++) {
if (loopOrder[pos] == id) return pos;
}
return 0;
}
uint8_t getColumnForPosition(uint8_t pos) {
uint8_t col = 0;
for (uint8_t i = 1; i <= pos; i++) {
uint8_t id = loopOrder[i];
if (!loopParallel[id]) {
col++;
}
}
return col;
}
uint8_t getColumnCount() {
uint8_t cols = 1;
for (uint8_t i = 1; i < NUM_LOOPS; i++) {
uint8_t id = loopOrder[i];
if (!loopParallel[id]) {
cols++;
}
}
return cols;
}
// Alles etwas nach links verschoben, damit Out komplett sichtbar bleibt.
int getBoxXByColumn(uint8_t col) {
if (col == 0) return 52;
if (col == 1) return 122;
return 192;
}
int getBoxYForLoopId(uint8_t id) {
if (loopParallel[id]) {
return BOT_Y;
}
return TOP_Y;
}
void drawBox(int x, int y, const char *txt, LoopMode m, bool selected, bool grabbed) {
tft.fillRect(x - 3, y - 3, BOX_W + 6, BOX_H + 6, ILI9341_BLACK);
tft.fillRect(x, y, BOX_W, BOX_H, boxColor(m));
uint16_t c = ILI9341_WHITE;
if (grabbed) {
c = ILI9341_MAGENTA;
} else if (selected) {
c = ILI9341_CYAN;
}
tft.drawRect(x, y, BOX_W, BOX_H, c);
if (selected) {
tft.drawRect(x - 2, y - 2, BOX_W + 4, BOX_H + 4, c);
}
tft.setTextSize(2);
tft.setTextColor(textColor(m));
tft.setCursor(x + 12, y + 14);
tft.print(txt);
}
void drawBoxAtPosition(uint8_t pos) {
if (pos >= NUM_LOOPS) return;
uint8_t id = loopOrder[pos];
uint8_t col = getColumnForPosition(pos);
int x = getBoxXByColumn(col);
int y = getBoxYForLoopId(id);
bool selected = (pos == encoderSelectedPos);
bool grabbed = selected && encoderGrabbed;
drawBox(x, y, loopLabels[id], loops[id].mode, selected, grabbed);
}
bool positionHasParallelChild(uint8_t pos) {
if (pos >= NUM_LOOPS - 1) {
return false;
}
uint8_t childId = loopOrder[pos + 1];
return loopParallel[childId];
}
uint8_t getGroupStartForPosition(uint8_t pos) {
if (pos > 0) {
uint8_t id = loopOrder[pos];
if (loopParallel[id]) {
return pos - 1;
}
}
return pos;
}
uint8_t getGroupLength(uint8_t groupStart) {
if (positionHasParallelChild(groupStart)) {
return 2;
}
return 1;
}
int getPreviousMergeXForGroup(uint8_t groupStart) {
if (groupStart == 0) {
return 30;
}
uint8_t prevStart = groupStart - 1;
if (loopParallel[loopOrder[prevStart]] && prevStart > 0) {
prevStart--;
}
uint8_t prevCol = getColumnForPosition(prevStart);
int prevX = getBoxXByColumn(prevCol);
return prevX + BOX_W + 8;
}
void drawStatus() {
#if ENABLE_STATUS_BOX
if (!screenReady) return;
tft.fillRect(205, 10, 110, 22, ILI9341_BLACK);
tft.drawRect(205, 10, 110, 22, ILI9341_WHITE);
tft.setTextSize(2);
if (systemMode == SYS_LEARN) {
tft.setTextColor(ILI9341_YELLOW);
tft.setCursor(215, 14);
tft.print("LEARN");
} else {
tft.setTextColor(ILI9341_WHITE);
tft.setCursor(225, 14);
tft.print("PLAY");
}
#endif
}
void drawGroupAtPosition(uint8_t groupStart) {
if (groupStart >= NUM_LOOPS) return;
bool hasParallel = positionHasParallelChild(groupStart);
uint8_t topId = loopOrder[groupStart];
uint8_t bottomId = 255;
if (hasParallel) {
bottomId = loopOrder[groupStart + 1];
}
uint8_t col = getColumnForPosition(groupStart);
int x = getBoxXByColumn(col);
int splitX = x - 8;
int mergeX = x + BOX_W + 8;
int previousMergeX = getPreviousMergeXForGroup(groupStart);
if (hasParallel) {
// Linie bis Split ohne Pfeilkopf, verhindert doppelten Pfeilkopf
tft.drawLine(previousMergeX, TOP_MID_Y, splitX, TOP_MID_Y, ILI9341_WHITE);
// Split links
tft.drawLine(splitX, TOP_MID_Y, splitX, BOT_MID_Y, ILI9341_WHITE);
// Pfeile zu beiden Loops
drawArrow(splitX, TOP_MID_Y, x - 3);
drawArrow(splitX, BOT_MID_Y, x - 3);
} else {
drawArrow(previousMergeX, TOP_MID_Y, x - 3);
}
// Obere Box
bool topSelected = (groupStart == encoderSelectedPos);
bool topGrabbed = topSelected && encoderGrabbed;
drawBox(
x,
TOP_Y,
loopLabels[topId],
loops[topId].mode,
topSelected,
topGrabbed
);
if (hasParallel) {
bool bottomSelected = ((groupStart + 1) == encoderSelectedPos);
bool bottomGrabbed = bottomSelected && encoderGrabbed;
drawBox(
x,
BOT_Y,
loopLabels[bottomId],
loops[bottomId].mode,
bottomSelected,
bottomGrabbed
);
// Merge rechts
tft.drawLine(x + BOX_W + 3, TOP_MID_Y, mergeX, TOP_MID_Y, ILI9341_WHITE);
tft.drawLine(x + BOX_W + 3, BOT_MID_Y, mergeX, BOT_MID_Y, ILI9341_WHITE);
tft.drawLine(mergeX, TOP_MID_Y, mergeX, BOT_MID_Y, ILI9341_WHITE);
} else {
tft.drawLine(x + BOX_W + 3, TOP_MID_Y, mergeX, TOP_MID_Y, ILI9341_WHITE);
}
// Ausgang zur naechsten Gruppe oder zu Out
uint8_t nextGroup = groupStart + getGroupLength(groupStart);
if (nextGroup < NUM_LOOPS) {
uint8_t nextCol = getColumnForPosition(nextGroup);
int nextX = getBoxXByColumn(nextCol);
int nextSplitX = nextX - 8;
if (positionHasParallelChild(nextGroup)) {
// Linie bis zum naechsten Split ohne Pfeilkopf
tft.drawLine(mergeX, TOP_MID_Y, nextSplitX, TOP_MID_Y, ILI9341_WHITE);
} else {
drawArrow(mergeX, TOP_MID_Y, nextX - 3);
}
} else {
drawArrow(mergeX, TOP_MID_Y, 274);
tft.setTextColor(ILI9341_WHITE);
tft.setTextSize(2);
tft.setCursor(280, TOP_MID_Y - 8);
tft.print("Out");
}
}
void redrawGroupForPosition(uint8_t pos) {
if (!screenReady) return;
if (pos >= NUM_LOOPS) return;
uint8_t groupStart = getGroupStartForPosition(pos);
uint8_t col = getColumnForPosition(groupStart);
int x = getBoxXByColumn(col);
int clearX = x - 18;
if (clearX < 28) clearX = 28;
int clearW = BOX_W + 44;
// Lokale Gruppe loeschen, nicht den ganzen Signalpfad
tft.fillRect(clearX, 75, clearW, 135, ILI9341_BLACK);
drawGroupAtPosition(groupStart);
}
void drawSignalPath() {
if (!screenReady) return;
tft.fillRect(0, 75, 320, 135, ILI9341_BLACK);
tft.setTextColor(ILI9341_WHITE);
tft.setTextSize(2);
tft.setCursor(2, TOP_MID_Y - 8);
tft.print("In");
uint8_t pos = 0;
while (pos < NUM_LOOPS) {
drawGroupAtPosition(pos);
pos += getGroupLength(pos);
}
}
void updateBox(uint8_t id) {
if (!screenReady) return;
if (screenUpdatesSuspended) {
pendingFullRedraw = true;
return;
}
uint8_t pos = getPositionForLoopId(id);
// Nur die betroffene serielle oder parallele Gruppe neu zeichnen
redrawGroupForPosition(pos);
}
void requestFullRedraw() {
if (!screenReady) return;
if (screenUpdatesSuspended) {
pendingFullRedraw = true;
return;
}
drawSignalPath();
drawStatus();
}
void drawScreen() {
tft.begin();
tft.setRotation(1);
screenReady = true;
drawSignalPath();
drawStatus();
}
// ==================================================
// Mode
// ==================================================
void setMode(uint8_t id, LoopMode m) {
loops[id].mode = m;
setLed(id);
updateBox(id);
}
// ==================================================
// TIP
// ==================================================
void releaseTip(uint8_t id) {
pinMode(loops[id].tip, INPUT_PULLUP);
}
void driveTipLow(uint8_t id) {
digitalWrite(loops[id].tip, LOW);
pinMode(loops[id].tip, OUTPUT);
}
void startTip(uint8_t id) {
if (loops[id].tipState != TIP_IDLE) return;
driveTipLow(id);
loops[id].tipTimer = millis();
loops[id].tipState = TIP_PULSE_ACTIVE;
}
// ==================================================
// Inputs
// ==================================================
bool ringIs0V(uint8_t id) {
return digitalRead(loops[id].ring) == RING_0V;
}
bool ringIs3V3(uint8_t id) {
return digitalRead(loops[id].ring) == RING_3V3;
}
bool ringRise3V3(uint8_t id) {
LoopChannel &l = loops[id];
bool current = digitalRead(l.ring);
bool rise = (l.lastRing == RING_0V && current == RING_3V3);
l.lastRing = current;
return rise;
}
bool switchPressed(uint8_t id) {
LoopChannel &l = loops[id];
bool raw = digitalRead(l.sw);
if (raw != l.swLastRaw) {
l.swLastRaw = raw;
l.swLastChange = millis();
}
if (millis() - l.swLastChange >= DEBOUNCE_MS) {
if (raw != l.swStable) {
l.swStable = raw;
if (l.swStable == SWITCH_PRESSED) {
return true;
}
}
}
return false;
}
// ==================================================
// Learn Button
// ==================================================
bool learnPressedEvent() {
bool raw = digitalRead(LEARN_SW);
unsigned long now = millis();
if (!learnReady) {
if (raw == HIGH) {
learnReady = true;
learnLastRaw = HIGH;
learnStable = HIGH;
learnLastChange = now;
}
return false;
}
if (raw != learnLastRaw) {
learnLastRaw = raw;
learnLastChange = now;
}
if (now - learnLastChange >= DEBOUNCE_MS) {
if (raw != learnStable) {
learnStable = raw;
if (learnStable == LOW) {
return true;
}
}
}
return false;
}
// ==================================================
// Encoder
// ==================================================
int8_t readEncoderDelta() {
bool a = digitalRead(ENC_A);
int8_t delta = 0;
if (a != encLastA) {
if (a == LOW) {
bool b = digitalRead(ENC_B);
delta = b == HIGH ? 1 : -1;
}
encLastA = a;
}
return delta;
}
bool encoderButtonPressedEvent() {
bool raw = digitalRead(ENC_SW);
if (raw != encLastButtonRaw) {
encLastButtonRaw = raw;
encButtonLastChange = millis();
}
if (millis() - encButtonLastChange >= DEBOUNCE_MS) {
if (raw != encButtonStable) {
encButtonStable = raw;
if (encButtonStable == LOW) {
return true;
}
}
}
return false;
}
void moveEncoderSelection(int8_t delta) {
uint8_t oldPos = encoderSelectedPos;
if (delta > 0 && encoderSelectedPos < NUM_LOOPS - 1) {
encoderSelectedPos++;
}
if (delta < 0 && encoderSelectedPos > 0) {
encoderSelectedPos--;
}
if (oldPos != encoderSelectedPos) {
redrawGroupForPosition(oldPos);
redrawGroupForPosition(encoderSelectedPos);
}
}
bool hasParallelChild(uint8_t pos) {
if (pos >= NUM_LOOPS - 1) {
return false;
}
uint8_t childId = loopOrder[pos + 1];
return loopParallel[childId];
}
void normalizeParallelLayout() {
loopParallel[loopOrder[0]] = false;
for (uint8_t pos = 1; pos < NUM_LOOPS; pos++) {
uint8_t id = loopOrder[pos];
uint8_t prevId = loopOrder[pos - 1];
if (loopParallel[id] && loopParallel[prevId]) {
loopParallel[id] = false;
}
}
}
void moveGrabbedLoop(int8_t delta) {
uint8_t pos = encoderSelectedPos;
uint8_t id = loopOrder[pos];
if (delta > 0) {
if (loopParallel[id]) {
loopParallel[id] = false;
normalizeParallelLayout();
drawSignalPath();
return;
}
if (hasParallelChild(pos)) {
uint8_t childId = loopOrder[pos + 1];
loopOrder[pos] = childId;
loopOrder[pos + 1] = id;
loopParallel[childId] = false;
loopParallel[id] = false;
encoderSelectedPos = pos + 1;
normalizeParallelLayout();
drawSignalPath();
return;
}
if (pos >= NUM_LOOPS - 1) {
return;
}
uint8_t nextPos = pos + 1;
uint8_t temp = loopOrder[pos];
loopOrder[pos] = loopOrder[nextPos];
loopOrder[nextPos] = temp;
loopParallel[loopOrder[pos]] = false;
loopParallel[id] = true;
encoderSelectedPos = nextPos;
normalizeParallelLayout();
drawSignalPath();
return;
}
if (delta < 0) {
if (loopParallel[id]) {
if (pos == 0) {
loopParallel[id] = false;
normalizeParallelLayout();
drawSignalPath();
return;
}
uint8_t prevPos = pos - 1;
uint8_t temp = loopOrder[pos];
loopOrder[pos] = loopOrder[prevPos];
loopOrder[prevPos] = temp;
loopParallel[id] = false;
loopParallel[loopOrder[pos]] = false;
encoderSelectedPos = prevPos;
normalizeParallelLayout();
drawSignalPath();
return;
}
if (hasParallelChild(pos)) {
uint8_t childId = loopOrder[pos + 1];
loopOrder[pos] = childId;
loopOrder[pos + 1] = id;
loopParallel[childId] = false;
loopParallel[id] = false;
encoderSelectedPos = pos + 1;
normalizeParallelLayout();
drawSignalPath();
return;
}
if (pos == 0) {
return;
}
loopParallel[id] = true;
normalizeParallelLayout();
drawSignalPath();
return;
}
}
void updateEncoder() {
if (pcRecallInProgress || systemMode != SYS_PLAY) {
readEncoderDelta();
encoderButtonPressedEvent();
return;
}
int8_t delta = readEncoderDelta();
if (delta != 0) {
if (encoderGrabbed) {
moveGrabbedLoop(delta);
} else {
moveEncoderSelection(delta);
}
}
if (encoderButtonPressedEvent()) {
encoderGrabbed = !encoderGrabbed;
redrawGroupForPosition(encoderSelectedPos);
}
}
void setupEncoder() {
pinMode(ENC_A, INPUT_PULLUP);
pinMode(ENC_B, INPUT_PULLUP);
pinMode(ENC_SW, INPUT_PULLUP);
encLastA = digitalRead(ENC_A);
encLastButtonRaw = digitalRead(ENC_SW);
encButtonStable = encLastButtonRaw;
encButtonLastChange = millis();
encoderSelectedPos = 0;
encoderGrabbed = false;
}
// ==================================================
// TIP State
// ==================================================
void updateTip(uint8_t id) {
LoopChannel &l = loops[id];
unsigned long now = millis();
if (l.tipState == TIP_PULSE_ACTIVE) {
if (now - l.tipTimer >= TIP_PULSE_MS) {
releaseTip(id);
l.tipTimer = now;
l.tipState = l.mode == MODE_OFF ? TIP_WAIT_ON : TIP_WAIT_OFF;
}
}
if (l.tipState == TIP_WAIT_ON) {
if (ringIs0V(id)) {
setMode(id, MODE_GREEN);
l.lastRing = digitalRead(l.ring);
l.tipState = TIP_IDLE;
return;
}
if (now - l.tipTimer >= RING_WAIT_MS) {
setMode(id, MODE_YELLOW);
l.lastRing = digitalRead(l.ring);
l.tipState = TIP_IDLE;
return;
}
}
if (l.tipState == TIP_WAIT_OFF) {
if (ringIs3V3(id)) {
setMode(id, MODE_OFF);
l.lastRing = digitalRead(l.ring);
l.tipState = TIP_IDLE;
return;
}
if (now - l.tipTimer >= RING_WAIT_MS) {
setMode(id, ringIs0V(id) ? MODE_GREEN : MODE_OFF);
l.lastRing = digitalRead(l.ring);
l.tipState = TIP_IDLE;
return;
}
}
}
// ==================================================
// Loop Logic
// ==================================================
void updateLoop(uint8_t id) {
LoopChannel &l = loops[id];
updateTip(id);
bool pressed = switchPressed(id);
bool rise = ringRise3V3(id);
if (l.tipState != TIP_IDLE) {
setLed(id);
return;
}
if (l.mode == MODE_OFF) {
if (ringIs0V(id)) {
setMode(id, MODE_GREEN);
return;
}
if (pressed) {
startTip(id);
return;
}
}
if (l.mode == MODE_GREEN) {
if (pressed) {
startTip(id);
return;
}
if (rise) {
setMode(id, MODE_OFF);
return;
}
}
if (l.mode == MODE_YELLOW) {
if (pressed) {
startTip(id);
return;
}
if (ringIs0V(id)) {
setMode(id, MODE_GREEN);
return;
}
}
setLed(id);
}
// ==================================================
// PC Recall Batch
// ==================================================
bool allTipsIdle() {
for (uint8_t i = 0; i < NUM_LOOPS; i++) {
if (loops[i].tipState != TIP_IDLE) {
return false;
}
}
return true;
}
void beginPcRecallDisplayBatch() {
screenUpdatesSuspended = true;
pendingFullRedraw = true;
pcRecallInProgress = true;
}
void finishPcRecallDisplayBatchIfReady() {
if (!pcRecallInProgress) return;
if (!allTipsIdle()) return;
pcRecallInProgress = false;
screenUpdatesSuspended = false;
if (pendingFullRedraw) {
pendingFullRedraw = false;
drawSignalPath();
drawStatus();
}
}
// ==================================================
// Learn / PC
// ==================================================
void armLearn() {
systemMode = SYS_LEARN;
encoderGrabbed = false;
for (uint8_t i = 0; i < NUM_LOOPS; i++) {
learnMode[i] = loops[i].mode;
learnOrder[i] = loopOrder[i];
learnParallel[i] = loopParallel[i];
}
drawStatus();
drawSignalPath();
}
void cancelLearn() {
systemMode = SYS_PLAY;
drawStatus();
drawSignalPath();
}
void updateLearn() {
if (learnPressedEvent()) {
if (systemMode == SYS_LEARN) {
cancelLearn();
} else {
armLearn();
}
}
}
void savePreset(uint8_t pc) {
for (uint8_t i = 0; i < NUM_LOOPS; i++) {
presetMode[pc][i] = learnMode[i];
presetOrder[pc][i] = learnOrder[i];
presetParallel[pc][i] = learnParallel[i];
}
presetValid[pc] = true;
systemMode = SYS_PLAY;
drawStatus();
}
void applyPcTargetToLoop(uint8_t id, LoopMode targetMode) {
if (loops[id].tipState != TIP_IDLE) return;
bool currentIsOff = loops[id].mode == MODE_OFF;
bool targetIsOff = targetMode == MODE_OFF;
if (targetIsOff && !currentIsOff) {
startTip(id);
}
if (!targetIsOff && currentIsOff) {
startTip(id);
}
}
void recallPreset(uint8_t pc) {
if (!presetValid[pc]) return;
encoderGrabbed = false;
beginPcRecallDisplayBatch();
for (uint8_t i = 0; i < NUM_LOOPS; i++) {
loopOrder[i] = presetOrder[pc][i];
loopParallel[i] = presetParallel[pc][i];
}
for (uint8_t i = 0; i < NUM_LOOPS; i++) {
applyPcTargetToLoop(i, (LoopMode)presetMode[pc][i]);
}
finishPcRecallDisplayBatchIfReady();
}
bool parsePC(char *s, uint8_t &pc) {
if (s[0] != 'P' && s[0] != 'p') return false;
if (s[1] != 'C' && s[1] != 'c') return false;
int val = 0;
uint8_t i = 2;
if (s[i] == 0) return false;
while (s[i] != 0) {
if (s[i] < '0' || s[i] > '9') return false;
val = val * 10 + (s[i] - '0');
if (val > 127) return false;
i++;
}
pc = (uint8_t)val;
return true;
}
void handlePC(uint8_t pc) {
if (systemMode == SYS_LEARN) {
savePreset(pc);
} else {
recallPreset(pc);
}
}
void updateSerial() {
while (Serial.available() > 0) {
char c = Serial.read();
if (c == '\n' || c == '\r') {
if (serialLen > 0) {
serialBuf[serialLen] = 0;
uint8_t pc;
if (parsePC(serialBuf, pc)) {
handlePC(pc);
}
serialLen = 0;
}
} else if (serialLen < sizeof(serialBuf) - 1) {
serialBuf[serialLen++] = c;
}
}
}
// ==================================================
// Setup
// ==================================================
void setupLoopPins(uint8_t id) {
LoopChannel &l = loops[id];
pinMode(l.ledR, OUTPUT);
pinMode(l.ledG, OUTPUT);
pinMode(l.ledB, OUTPUT);
pinMode(l.sw, INPUT_PULLUP);
pinMode(l.ring, INPUT_PULLUP);
releaseTip(id);
l.mode = MODE_OFF;
l.tipState = TIP_IDLE;
setLed(id);
l.swLastRaw = digitalRead(l.sw);
l.swStable = l.swLastRaw;
l.swLastChange = millis();
l.lastRing = digitalRead(l.ring);
}
void setupPresets() {
for (uint8_t pc = 0; pc < PC_COUNT; pc++) {
presetValid[pc] = false;
for (uint8_t i = 0; i < NUM_LOOPS; i++) {
presetMode[pc][i] = MODE_OFF;
presetOrder[pc][i] = i;
presetParallel[pc][i] = false;
}
}
}
void setupLearnSwitch() {
pinMode(LEARN_SW, INPUT_PULLUP);
learnLastRaw = digitalRead(LEARN_SW);
learnStable = learnLastRaw;
learnLastChange = millis();
learnReady = learnStable == HIGH;
}
void setup() {
Serial.begin(115200);
delay(300);
for (uint8_t i = 0; i < NUM_LOOPS; i++) {
setupLoopPins(i);
}
setupLearnSwitch();
setupEncoder();
systemMode = SYS_PLAY;
drawScreen();
}
void loop() {
updateLearn();
updateEncoder();
updateSerial();
for (uint8_t i = 0; i < NUM_LOOPS; i++) {
updateLoop(i);
}
finishPcRecallDisplayBatchIfReady();
delay(5);
}