//#include <DFRobotDFPlayerMini.h>
#include <Adafruit_NeoPixel.h>
#include <SoftwareSerial.h>
#include "FakeDFPlayerMini.h"
// -------------------- CONFIG --------------------
#define NEOPIXEL_PIN 6
#define NEOPIXEL_COUNT 1
#define DF_RX 2 // Nano pin → DFPlayer TX
#define DF_TX 3 // Nano pin → DFPlayer RX (via voltage divider!)
#define NUM_ROWS 9
#define NUM_COLS 6
// Assign your actual Nano pins here for rows/columns
byte rowPins[NUM_ROWS] = {A0, A1, A2, A3, A4, A5, 4, 5, 7};
byte colPins[NUM_COLS] = {8, 9, 10, 11, 12, 13};
// -------------------- OBJECTS --------------------
SoftwareSerial dfSerial(DF_RX, DF_TX);
//DFRobotDFPlayerMini dfplayer;
FakeDFPlayerMini dfplayer;
Adafruit_NeoPixel pixel(NEOPIXEL_COUNT, NEOPIXEL_PIN, NEO_GRB + NEO_KHZ800);
// -------------------- STATE --------------------
enum Mode { PLAY, TEST };
Mode currentMode = PLAY;
int maxTestNumber = 33;
int currentFile = 0;
unsigned long fileStartTime = 0;
const unsigned long TIMEOUT = 10000; // 10 sec timeout
int volume = 20; // default volume
// -------------------- Neopixel --------------------
void setPixelColor(uint8_t r, uint8_t g, uint8_t b) {
pixel.setPixelColor(0, pixel.Color(r, g, b));
pixel.show();
}
// -------------------- Mapping --------------------
int mapButton(int row, int col) {
const int buttonMap[9][6] = {
{34, 35, 36, 0, 0, 0}, // row 0
{37, 38, 39, 0, 0, 100}, // row 1 (100 = v+)
{40, 41, 42, 0, 0, 101}, // row 2 (101 = v-)
{ 6, 5, 4, 3, 2, 1}, // row 3
{12, 11, 10, 9, 8, 7}, // row 4
{18, 17, 16, 15, 14, 13}, // row 5
{24, 23, 22, 21, 20, 19}, // row 6
{ 0, 29, 28, 27, 26, 25}, // row 7
{102, 0, 33, 32, 31, 30} // row 8 (102 = mode)
};
return buttonMap[row][col];
}
// -------------------- Matrix scan (debounced, evented) --------------------
int scanMatrix() {
static int lastStable = 0; // last stable debounced button
static int lastReported = 0; // last button we told loop() about
static unsigned long lastChange = 0;
const unsigned long debounceDelay = 50;
int reading = 0;
// --- raw matrix scan ---
for (int c = 0; c < NUM_COLS; c++) {
digitalWrite(colPins[c], LOW);
for (int r = 0; r < NUM_ROWS; r++) {
if (digitalRead(rowPins[r]) == LOW) {
reading = mapButton(r, c);
break;
}
}
digitalWrite(colPins[c], HIGH);
if (reading) break;
}
// debouncing logic
if (reading != lastStable) {
lastChange = millis(); // reset debounce timer
lastStable = reading;
}
if ((millis() - lastChange) > debounceDelay) {
// After debounce time, reading is stable
if (lastStable != lastReported) {
lastReported = lastStable;
if (lastReported != 0) {
Serial.print("Debounced press: ");
Serial.println(lastReported);
return lastReported; // return *only once when pressed*
} else {
Serial.println("Button released");
}
}
}
return 0; // no new press event
}
// -------------------- Helpers --------------------
void startNewTestQuestion() {
currentFile = random(1, maxTestNumber + 1);
Serial.print("TEST → new question: ");
Serial.println(currentFile);
dfplayer.play(currentFile);
fileStartTime = millis();
setPixelColor(255, 128, 0); // orange
}
int rawStartupScan() {
for (int c = 0; c < NUM_COLS; c++) {
digitalWrite(colPins[c], LOW);
for (int r = 0; r < NUM_ROWS; r++) {
if (digitalRead(rowPins[r]) == LOW) {
digitalWrite(colPins[c], HIGH);
return mapButton(r, c);
}
}
digitalWrite(colPins[c], HIGH);
}
return 0;
}
// -------------------- SETUP --------------------
void setup() {
randomSeed(A7);
Serial.begin(9600);
Serial.println("System starting...");
// matrix pins
for (int c = 0; c < NUM_COLS; c++) {
pinMode(colPins[c], OUTPUT);
digitalWrite(colPins[c], HIGH);
}
for (int r = 0; r < NUM_ROWS; r++) {
pinMode(rowPins[r], INPUT_PULLUP);
}
// neopixel
pixel.begin();
setPixelColor(0, 0, 255); // blue = startup
// DFPlayer (fake)
dfSerial.begin(9600);
if (!dfplayer.begin(dfSerial)) {
Serial.println("DFPlayer init failed!");
while (true) {
setPixelColor(255, 0, 0);
delay(300);
setPixelColor(0, 0, 0);
delay(300);
}
}
dfplayer.volume(volume);
Serial.print("Initial volume: ");
Serial.println(volume);
// Startup limit (kept simple)
maxTestNumber = 33;
for (int i = 1; i <= 33; i++) {
if (rawStartupScan() == i) {
maxTestNumber = i;
Serial.print("Startup limit set to ");
Serial.println(maxTestNumber);
break;
}
}
}
// -------------------- LOOP --------------------
void loop() {
int button = scanMatrix();
if (button != 0) {
Serial.print("Loop sees button: ");
Serial.println(button);
}
// ---- Special buttons ----
if (button == 102) { // MODE
if (currentMode == PLAY) {
currentMode = TEST;
Serial.println("Mode changed to TEST");
setPixelColor(255, 128, 0);
currentFile = 0; // reset test state
startNewTestQuestion(); // <-- start immediately
} else {
currentMode = PLAY;
Serial.println("Mode changed to PLAY");
setPixelColor(0, 0, 255);
currentFile = 0; // stop test engine
}
delay(250);
return; // done handling this press
}
if (button == 100) { // V+
volume = min(volume + 1, 30);
dfplayer.volume(volume);
Serial.print("Volume up → ");
Serial.println(volume);
delay(150);
return;
}
if (button == 101) { // V-
volume = max(volume - 1, 0);
dfplayer.volume(volume);
Serial.print("Volume down → ");
Serial.println(volume);
delay(150);
return;
}
// ---- PLAY MODE ----
if (currentMode == PLAY) {
if (button >= 1 && button <= 33) {
Serial.print("PLAY → play file ");
Serial.println(button);
dfplayer.play(button);
}
return; // nothing else to do in PLAY
}
// ---- TEST MODE ----
// Always run the TEST engine, even if no button was pressed
if (currentMode == TEST) {
// If no active question, start one (covers any case where currentFile was reset)
if (currentFile == 0) {
startNewTestQuestion();
}
// Timeout + pulsing (runs continuously)
unsigned long elapsed = millis() - fileStartTime;
if (elapsed > TIMEOUT) {
Serial.println("Timeout → replaying current question");
dfplayer.play(currentFile);
fileStartTime = millis();
elapsed = 0;
}
float progress = (float)elapsed / TIMEOUT; // 0..1
int brightness = 128 + int(127 * sin(progress * PI * 2));
pixel.setPixelColor(0, pixel.Color(brightness, brightness / 2, 0)); // orange pulse
pixel.show();
// Handle an answer if a number button was pressed
if (button >= 1 && button <= 33) {
if (button == currentFile) {
Serial.println("TEST → correct!");
dfplayer.play(340);
for (int i = 0; i < 3; i++) {
setPixelColor(0, 255, 0);
delay(250);
setPixelColor(0, 0, 0);
delay(250);
}
// immediate next question
startNewTestQuestion();
} else {
Serial.print("TEST → wrong (expected ");
Serial.print(currentFile);
Serial.print(", got ");
Serial.print(button);
Serial.println(")");
dfplayer.play(341);
for (int i = 0; i < 3; i++) {
setPixelColor(255, 0, 0);
delay(250);
setPixelColor(0, 0, 0);
delay(250);
}
dfplayer.play(currentFile); // replay same question
fileStartTime = millis();
setPixelColor(255, 128, 0);
}
}
}
}