#include <Arduino.h>
#include <U8g2lib.h>
#include <Wire.h>
#include <Preferences.h>
// --- PINOUT ---
const int swPins[6] = {35, 36, 37, 38, 40, 39};
const int tapPin = 41;
const int ledPins[4] = {15, 16, 17, 18};
#define MIDI_TX_PIN 43
U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, /* reset=*/ U8X8_PIN_NONE, /* clock=*/ 2, /* data=*/ 1);
Preferences prefs;
// --- STATE MANAGEMENT ---
int currentBank = 1, activeBank = 1, activePreset = 0, deviceMode = 0;
bool inMenu = false, isBlinking = false, blinkState = true;
unsigned long lastBlinkTime = 0;
bool lastSwState[6] = {HIGH, HIGH, HIGH, HIGH, HIGH, HIGH};
bool lastTapState = HIGH;
// Tap Tempo Logic
unsigned long lastTapTime = 0, lastTapActivity = 0;
int currentBPM = 120;
bool isTapModeActive = false;
const int tapTimeout = 1200;
const char* deviceNames[] = {"NUX MG-300", "VALETON GP", "BOSS GT-1"};
const int maxBanks[] = {9, 32, 50};
// --- MIDI ENGINE ---
void sendMIDI(byte status, byte data1, byte data2) {
Serial1.write(status);
Serial1.write(data1);
if ((status & 0xF0) != 0xC0) Serial1.write(data2);
}
void executeMIDI(int bank, int preset) {
int pcNum = ((bank - 1) * 4) + preset;
if (deviceMode == 1) {
sendMIDI(0xB0, 0, (pcNum > 127) ? 1 : 0);
sendMIDI(0xC0, pcNum % 128, 0);
} else {
sendMIDI(0xC0, pcNum, 0);
}
}
// --- DISPLAY ENGINE ---
void refreshDisplay() {
u8g2.clearBuffer();
if (isTapModeActive) {
u8g2.setFont(u8g2_font_7x14_tr);
u8g2.drawStr(22, 15, "BON-BON TAP");
u8g2.drawLine(0, 18, 128, 18);
u8g2.setFont(u8g2_font_logisoso32_tf);
char bpmStr[5]; sprintf(bpmStr, "%d", currentBPM);
u8g2.drawStr((128 - u8g2.getStrWidth(bpmStr)) / 2, 55, bpmStr);
} else if (inMenu) {
u8g2.setFont(u8g2_font_7x14_tr);
u8g2.drawStr(25, 18, "DEVICE SET");
u8g2.drawFrame(5, 25, 118, 25);
u8g2.drawStr(15, 42, deviceNames[deviceMode]);
u8g2.setFont(u8g2_font_5x7_tr);
u8g2.drawStr(10, 60, "P3:CHANGE P4:SAVE");
} else {
u8g2.setDrawColor(1);
u8g2.drawRBox(0, 0, 128, 15, 2);
u8g2.setDrawColor(0);
u8g2.setFont(u8g2_font_haxrcorp4089_tr);
u8g2.drawStr((128 - u8g2.getStrWidth(deviceNames[deviceMode])) / 2, 12, deviceNames[deviceMode]);
u8g2.setDrawColor(1);
u8g2.drawLine(64, 18, 64, 62);
u8g2.setFont(u8g2_font_6x12_tr);
u8g2.drawStr(20, 26, "BANK"); u8g2.drawStr(80, 26, "PRESET");
u8g2.setFont(u8g2_font_logisoso32_tf);
if (!isBlinking || blinkState) {
char bStr[3]; sprintf(bStr, "%02d", currentBank);
u8g2.drawStr((64 - u8g2.getStrWidth(bStr)) / 2, 60, bStr);
}
if (!isBlinking) {
char pStr[2] = {(char)('A' + activePreset), '\0'};
u8g2.drawStr(64 + (64 - u8g2.getStrWidth(pStr)) / 2, 60, pStr);
}
}
u8g2.sendBuffer();
}
void commitPreset(int p) {
activePreset = p;
activeBank = currentBank;
executeMIDI(activeBank, activePreset);
for(int i=0; i<4; i++) digitalWrite(ledPins[i], (i == p) ? HIGH : LOW);
isBlinking = false;
isTapModeActive = false;
refreshDisplay();
}
// --- SETUP ---
void setup() {
Serial1.begin(31250, SERIAL_8N1, -1, MIDI_TX_PIN);
u8g2.begin();
u8g2.clearBuffer();
u8g2.setFont(u8g2_font_ncenB14_tr);
u8g2.drawStr(25, 40, "BON-BON");
u8g2.sendBuffer();
delay(500);
for(int i=0; i<6; i++) pinMode(swPins[i], INPUT_PULLUP);
pinMode(tapPin, INPUT_PULLUP);
for(int i=0; i<4; i++) { pinMode(ledPins[i], OUTPUT); digitalWrite(ledPins[i], LOW); }
prefs.begin("midi-cfg", true);
deviceMode = prefs.getInt("devMode", 0);
prefs.end();
commitPreset(0);
}
void loop() {
unsigned long now = millis();
// 1. Blink Handling
if (isBlinking && (now - lastBlinkTime > 200)) {
blinkState = !blinkState; lastBlinkTime = now;
if (!isTapModeActive && !inMenu) refreshDisplay();
}
// 2. Tap Timeout
if (isTapModeActive && (now - lastTapActivity > tapTimeout)) {
isTapModeActive = false; refreshDisplay();
}
// 3. Tap Logic
bool tapRead = digitalRead(tapPin);
if (tapRead == LOW && lastTapState == HIGH) {
unsigned long diff = now - lastTapTime;
if (diff < 2000 && diff > 150) currentBPM = 60000 / diff;
lastTapTime = now; lastTapActivity = now;
isTapModeActive = true;
sendMIDI(0xB0, 64, 127);
refreshDisplay();
}
lastTapState = tapRead;
// --- LOGIKA ANTI-GLITCH TOMBOL ---
if (!inMenu) {
// Cek Menu dulu sebelum proses Preset A
if (digitalRead(swPins[0]) == LOW && digitalRead(swPins[1]) == LOW) {
inMenu = true;
delay(300); // Tunggu sampai tangan lepas
refreshDisplay();
return;
}
for (int i = 0; i < 6; i++) {
bool swRead = digitalRead(swPins[i]);
if (swRead == LOW && lastSwState[i] == HIGH) {
// Jika P1 ditekan, tunggu 20ms sebentar untuk pastikan bukan mau tekan P2 (Menu)
if (i == 0) {
delay(20);
if (digitalRead(swPins[1]) == LOW) { lastSwState[i] = LOW; continue; }
}
if (i < 4) {
commitPreset(i);
} else if (i == 4) {
if (currentBank > 1) { currentBank--; isBlinking = true; refreshDisplay(); }
} else if (i == 5) {
if (currentBank < maxBanks[deviceMode]) { currentBank++; isBlinking = true; refreshDisplay(); }
}
}
lastSwState[i] = swRead;
}
} else {
// Logika Menu
if (digitalRead(swPins[2]) == LOW) { deviceMode = (deviceMode + 1) % 3; refreshDisplay(); delay(250); }
if (digitalRead(swPins[3]) == LOW) {
prefs.begin("midi-cfg", false); prefs.putInt("devMode", deviceMode); prefs.end();
inMenu = false; commitPreset(0);
}
}
}