#include <LiquidCrystal_I2C.h>
#include "thingProperties.h"
// ===================== LCD =====================
#define I2C_ADDR 0x27
LiquidCrystal_I2C lcd(I2C_ADDR, 20, 4);
SemaphoreHandle_t lcdMutex = NULL;
// ===================== PINS =====================
#define BUZZER_PIN 25
#define REWIND_PIN 26
#define PLAY_PIN 27
#define PAUSE_PIN 14
#define FORWARD_PIN 12
#define JOY_X_PIN 34
#define JOY_Y_PIN 35
// ===================== FreeRTOS =====================
TaskHandle_t taskButtonsHandle = NULL;
TaskHandle_t taskMusicHandle = NULL;
TaskHandle_t taskDisplayHandle = NULL;
TaskHandle_t taskJoystickHandle = NULL;
// ===================== NOTES =====================
#define NOTE_B3 247
#define NOTE_C4 262
#define NOTE_D4 294
#define NOTE_E4 330
#define NOTE_F4 349
#define NOTE_G4 392
#define NOTE_A4 440
#define NOTE_B4 494
#define NOTE_C5 523
#define NOTE_D5 587
#define NOTE_E5 659
#define NOTE_F5 698
#define NOTE_G5 784
#define NOTE_A5 880
struct Note {
int freq;
int dur;
};
struct Song {
const char* name;
const Note* notes;
int length;
};
// ===================== SONGS =====================
const Note song1[] = {
{NOTE_C4, 250}, {NOTE_E4, 250}, {NOTE_G4, 250}, {0, 80},
{NOTE_C5, 450}, {0, 120},
{NOTE_G4, 250}, {NOTE_E4, 250}, {NOTE_C4, 450}, {0, 150}
};
const Note song2[] = {
{NOTE_E4, 220}, {NOTE_D4, 220}, {NOTE_C4, 220}, {NOTE_D4, 220},
{NOTE_E4, 220}, {NOTE_E4, 220}, {NOTE_E4, 450}, {0, 120},
{NOTE_D4, 220}, {NOTE_D4, 220}, {NOTE_D4, 450}, {0, 120}
};
const Note song3[] = {
{NOTE_G4, 200}, {NOTE_A4, 200}, {NOTE_B4, 200}, {NOTE_D5, 350},
{NOTE_B4, 200}, {NOTE_A4, 200}, {NOTE_G4, 400}, {0, 120},
{NOTE_G4, 200}, {NOTE_A4, 200}, {NOTE_B4, 200}, {NOTE_G5, 500}
};
const Note song4[] = {
{NOTE_C5, 180}, {NOTE_B4, 180}, {NOTE_A4, 180}, {NOTE_G4, 180},
{NOTE_A4, 180}, {NOTE_B4, 180}, {NOTE_C5, 350}, {0, 100},
{NOTE_E5, 200}, {NOTE_D5, 200}, {NOTE_C5, 500}
};
const Song songs[] = {
{"Melody One", song1, sizeof(song1) / sizeof(song1[0])},
{"Melody Two", song2, sizeof(song2) / sizeof(song2[0])},
{"Melody Three", song3, sizeof(song3) / sizeof(song3[0])},
{"Melody Four", song4, sizeof(song4) / sizeof(song4[0])}
};
const int SONG_COUNT = sizeof(songs) / sizeof(songs[0]);
// ===================== PLAYER STATE =====================
enum PlayerState {
STATE_PLAYING,
STATE_PAUSED
};
volatile int currentSongIndex = 0;
volatile int currentNoteIndex = 0;
volatile int remainingNoteMs = 0;
volatile PlayerState playerState = STATE_PAUSED;
// ===================== JOYSTICK GAME =====================
char gameField[2][21];
volatile int playerX = 10;
volatile int playerY = 0;
volatile int remainingOs = 0;
volatile int successfulRounds = 0;
volatile bool messageMode = false;
// ===================== LCD HELPERS =====================
void lcdLock() {
if (lcdMutex != NULL) {
xSemaphoreTake(lcdMutex, portMAX_DELAY);
}
}
void lcdUnlock() {
if (lcdMutex != NULL) {
xSemaphoreGive(lcdMutex);
}
}
String fit20(const String &text) {
String s = text;
if (s.length() < 20) {
while (s.length() < 20) s += ' ';
} else if (s.length() > 20) {
s = s.substring(0, 20);
}
return s;
}
void printLineUnsafe(int row, const String &text) {
lcd.setCursor(0, row);
lcd.print(fit20(text));
}
void printLine(int row, const String &text) {
lcdLock();
printLineUnsafe(row, text);
lcdUnlock();
}
void clearLcdSafe() {
lcdLock();
lcd.clear();
lcdUnlock();
}
void drawGameFieldUnsafe() {
for (int row = 0; row < 2; row++) {
lcd.setCursor(0, row + 2);
for (int col = 0; col < 20; col++) {
if (col == playerX && row == playerY) lcd.print("X");
else lcd.print(gameField[row][col]);
}
}
}
void drawGameField() {
lcdLock();
drawGameFieldUnsafe();
lcdUnlock();
}
// ===================== HELPERS =====================
const char* stateToText() {
return (playerState == STATE_PLAYING) ? "PLAYING" : "PAUSED";
}
void syncCloudState() {
songNameCloud = songs[currentSongIndex].name;
playerStateText = stateToText();
songIndexCloud = currentSongIndex + 1;
successfulRoundsCloud = successfulRounds;
}
// ===================== PLAYER COMMANDS =====================
void playCommand() {
playerState = STATE_PLAYING;
syncCloudState();
}
void pauseCommand() {
playerState = STATE_PAUSED;
noTone(BUZZER_PIN);
syncCloudState();
}
void rewindCommand() {
noTone(BUZZER_PIN);
currentSongIndex--;
if (currentSongIndex < 0) currentSongIndex = SONG_COUNT - 1;
currentNoteIndex = 0;
remainingNoteMs = 0;
playerState = STATE_PLAYING;
syncCloudState();
}
void forwardCommand() {
noTone(BUZZER_PIN);
currentSongIndex++;
if (currentSongIndex >= SONG_COUNT) currentSongIndex = 0;
currentNoteIndex = 0;
remainingNoteMs = 0;
playerState = STATE_PLAYING;
syncCloudState();
}
// ===================== CLOUD CALLBACKS =====================
void onCmdRewindChange() {
if (cmdRewind) {
rewindCommand();
cmdRewind = false;
}
}
void onCmdPlayChange() {
if (cmdPlay) {
playCommand();
cmdPlay = false;
}
}
void onCmdPauseChange() {
if (cmdPause) {
pauseCommand();
cmdPause = false;
}
}
void onCmdForwardChange() {
if (cmdForward) {
forwardCommand();
cmdForward = false;
}
}
// ===================== GAME =====================
void initGameField() {
remainingOs = 0;
playerX = 10;
playerY = 0;
for (int row = 0; row < 2; row++) {
for (int col = 0; col < 20; col++) {
gameField[row][col] = 'O';
remainingOs++;
}
gameField[row][20] = '\0';
}
if (gameField[playerY][playerX] == 'O') {
gameField[playerY][playerX] = ' ';
remainingOs--;
}
drawGameField();
syncCloudState();
}
void showRoundMessage() {
messageMode = true;
lcdLock();
lcd.clear();
printLineUnsafe(0, "All O collected!");
printLineUnsafe(1, "Successful tries:");
printLineUnsafe(2, String(successfulRounds));
printLineUnsafe(3, "New field loading");
lcdUnlock();
vTaskDelay(pdMS_TO_TICKS(1800));
lcdLock();
lcd.clear();
lcdUnlock();
messageMode = false;
}
// ===================== TASKS =====================
void TaskButtons(void *pvParameters) {
bool lastRewind = true;
bool lastPlay = true;
bool lastPause = true;
bool lastForward = true;
while (true) {
bool nowRewind = digitalRead(REWIND_PIN);
bool nowPlay = digitalRead(PLAY_PIN);
bool nowPause = digitalRead(PAUSE_PIN);
bool nowForward = digitalRead(FORWARD_PIN);
if (lastRewind && !nowRewind) rewindCommand();
if (lastPlay && !nowPlay) playCommand();
if (lastPause && !nowPause) pauseCommand();
if (lastForward && !nowForward) forwardCommand();
lastRewind = nowRewind;
lastPlay = nowPlay;
lastPause = nowPause;
lastForward = nowForward;
vTaskDelay(pdMS_TO_TICKS(60));
}
}
void TaskMusicPlayer(void *pvParameters) {
while (true) {
if (playerState != STATE_PLAYING) {
noTone(BUZZER_PIN);
vTaskDelay(pdMS_TO_TICKS(20));
continue;
}
const Song &song = songs[currentSongIndex];
if (currentNoteIndex >= song.length) {
currentSongIndex++;
if (currentSongIndex >= SONG_COUNT) currentSongIndex = 0;
currentNoteIndex = 0;
remainingNoteMs = 0;
syncCloudState();
continue;
}
if (remainingNoteMs <= 0) {
Note n = song.notes[currentNoteIndex];
if (n.freq > 0) tone(BUZZER_PIN, n.freq);
else noTone(BUZZER_PIN);
remainingNoteMs = n.dur;
}
int localRemaining = remainingNoteMs;
int chunk = (localRemaining < 10) ? localRemaining : 10;
vTaskDelay(pdMS_TO_TICKS(chunk));
if (playerState == STATE_PLAYING) {
remainingNoteMs -= chunk;
if (remainingNoteMs <= 0) {
noTone(BUZZER_PIN);
currentNoteIndex++;
vTaskDelay(pdMS_TO_TICKS(25));
}
}
}
}
void TaskDisplay(void *pvParameters) {
String prevLine0 = "";
String prevLine1 = "";
while (true) {
if (!messageMode) {
String line0 = "Song: " + String(songs[currentSongIndex].name);
String line1 = "State: " + String(stateToText());
if (line0 != prevLine0 || line1 != prevLine1) {
lcdLock();
printLineUnsafe(0, line0);
printLineUnsafe(1, line1);
lcdUnlock();
prevLine0 = line0;
prevLine1 = line1;
}
}
vTaskDelay(pdMS_TO_TICKS(120));
}
}
void TaskJoystick(void *pvParameters) {
initGameField();
int lastMoveTime = 0;
while (true) {
if (messageMode) {
vTaskDelay(pdMS_TO_TICKS(100));
continue;
}
int valueX = analogRead(JOY_X_PIN);
int valueY = analogRead(JOY_Y_PIN);
int newX = playerX;
int newY = playerY;
if (millis() - lastMoveTime > 170) {
if (valueX < 1000 && playerX > 0) {
newX--;
lastMoveTime = millis();
} else if (valueX > 3000 && playerX < 19) {
newX++;
lastMoveTime = millis();
} else if (valueY < 1000 && playerY < 1) {
newY++;
lastMoveTime = millis();
} else if (valueY > 3000 && playerY > 0) {
newY--;
lastMoveTime = millis();
}
}
if (newX != playerX || newY != playerY) {
lcdLock();
lcd.setCursor(playerX, playerY + 2);
lcd.print(gameField[playerY][playerX]);
playerX = newX;
playerY = newY;
if (gameField[playerY][playerX] == 'O') {
gameField[playerY][playerX] = ' ';
remainingOs--;
}
lcd.setCursor(playerX, playerY + 2);
lcd.print("X");
lcdUnlock();
if (remainingOs <= 0) {
successfulRounds++;
syncCloudState();
showRoundMessage();
lcdLock();
printLineUnsafe(0, "Song: " + String(songs[currentSongIndex].name));
printLineUnsafe(1, "State: " + String(stateToText()));
drawGameFieldUnsafe();
lcdUnlock();
initGameField();
}
}
vTaskDelay(pdMS_TO_TICKS(30));
}
}
// ===================== SETUP / LOOP =====================
void setup() {
Serial.begin(115200);
delay(1500);
pinMode(REWIND_PIN, INPUT_PULLUP);
pinMode(PLAY_PIN, INPUT_PULLUP);
pinMode(PAUSE_PIN, INPUT_PULLUP);
pinMode(FORWARD_PIN, INPUT_PULLUP);
pinMode(BUZZER_PIN, OUTPUT);
lcdMutex = xSemaphoreCreateMutex();
lcd.init();
lcd.backlight();
clearLcdSafe();
printLine(0, "Arduino Cloud");
printLine(1, "Starting...");
initProperties();
ArduinoCloud.begin(ArduinoIoTPreferredConnection);
setDebugMessageLevel(2);
ArduinoCloud.printDebugInfo();
syncCloudState();
xTaskCreatePinnedToCore(TaskButtons, "TaskButtons", 4096, NULL, 1, &taskButtonsHandle, 1);
xTaskCreatePinnedToCore(TaskMusicPlayer, "TaskMusic", 4096, NULL, 1, &taskMusicHandle, 0);
xTaskCreatePinnedToCore(TaskDisplay, "TaskDisplay", 4096, NULL, 1, &taskDisplayHandle, 1);
xTaskCreatePinnedToCore(TaskJoystick, "TaskJoystick", 4096, NULL, 1, &taskJoystickHandle, 1);
}
void loop() {
ArduinoCloud.update();
delay(10);
}Rewind
Play
Pause
Forward