Tue. Jul 28th, 2026

The controller (WIO Terminal)

#include <rpcWiFi.h>
#include <WiFiUdp.h>
#include <TFT_eSPI.h>

#include "config.h" // Pulls in WIFI_SSID, WIFI_PASSWORD, and UDP_PORT

// UDP Configuration
WiFiUDP udp;

// TFT Screen
TFT_eSPI tft = TFT_eSPI();

// UI State Variables
int selectedTrack = 1;
const int maxTracks = 10; 

void setup() {
  Serial.begin(115200);
  tft.begin();
  tft.setRotation(3);
  tft.fillScreen(TFT_BLACK);
  tft.setTextSize(2);
  tft.setTextColor(TFT_GREEN);
  tft.drawString("Connecting to Wi-Fi...", 10, 10);

  // Configure Joystick & Top Buttons as inputs (Fixed naming to WIO_5S)
  pinMode(WIO_5S_UP, INPUT_PULLUP);
  pinMode(WIO_5S_DOWN, INPUT_PULLUP);
  pinMode(WIO_KEY_A, INPUT_PULLUP); // Right-most top button
  pinMode(WIO_KEY_B, INPUT_PULLUP); // Middle top button

  // Updated to use uppercase names from config.h
  WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }

  udp.begin(UDP_PORT); // Updated to match config.h
  updateScreen();
}

void loop() {
  // Joystick UP: Increment Track Number
  if (digitalRead(WIO_5S_UP) == LOW) {
    if (selectedTrack < maxTracks) {
      selectedTrack++;
      updateScreen();
      delay(200); // Debounce
    }
  }

  // Joystick DOWN: Decrement Track Number
  if (digitalRead(WIO_5S_DOWN) == LOW) {
    if (selectedTrack > 1) {
      selectedTrack--;
      updateScreen();
      delay(200);
    }
  }

  // Button A (Right Top Button): SEND PLAY COMMAND
  if (digitalRead(WIO_KEY_A) == LOW) {
    sendUDPCommand("PLAY:" + String(selectedTrack));
    tft.setTextColor(TFT_RED);
    tft.drawString("SENT: PLAY " + String(selectedTrack), 10, 150);
    delay(400);
    updateScreen();
  }

  // Button B (Middle Top Button): SEND STOP COMMAND
  if (digitalRead(WIO_KEY_B) == LOW) {
    sendUDPCommand("STOP");
    tft.setTextColor(TFT_YELLOW);
    tft.drawString("SENT: STOP      ", 10, 150);
    delay(400);
    updateScreen();
  }
}

void updateScreen() {
  tft.fillScreen(TFT_BLACK);
  tft.setTextColor(TFT_GREEN);
  tft.drawString("HALLOWEEN AUDIO MATRIX", 10, 10);
  
  // Network Debug Info
  tft.setTextSize(1);
  tft.setTextColor(TFT_YELLOW);
  tft.drawString("SSID: " + String(WiFi.SSID()), 10, 35);
  tft.drawString("IP:   " + WiFi.localIP().toString(), 10, 48);
  tft.drawString("-----------------------------------", 10, 60);
  
  tft.setTextSize(2);
  tft.setTextColor(TFT_WHITE);
  tft.drawString("Selected Track: " + String(selectedTrack), 10, 80);
  
  tft.setTextSize(1);
  tft.drawString("Press Top Right Button (A) to PLAY", 10, 120);
  tft.drawString("Press Top Middle Button (B) to STOP", 10, 135);
  tft.setTextSize(2);
}

void sendUDPCommand(String msg) {
  // Pass direct IPAddress object for subnet broadcast
  udp.beginPacket(IPAddress(255, 255, 255, 255), UDP_PORT);
  udp.print(msg);
  udp.endPacket();
  Serial.println("Broadcasted: " + msg);
}

The MP3 Player (M5Stack AtomS3U plus Grove MP3 v4.0)

#include "../config.h"

#include <Adafruit_NeoPixel.h>
#include <WiFi.h>
#include <WiFiUdp.h>

// --- Hardware Pins ---
#define LED_PIN         35
#define NUM_LEDS        1
// AtomS3U Grove port UART pins for the MP3 player
#define GROVE_RX_PIN    1
#define GROVE_TX_PIN    2

Adafruit_NeoPixel rgb(NUM_LEDS, LED_PIN, NEO_GRB + NEO_KHZ800);
WiFiUDP udp;

// --- Status Color Functions ---
void setStatusRed()    { rgb.setPixelColor(0, rgb.Color(255, 0, 0));   rgb.show(); } // Booted
void setStatusYellow() { rgb.setPixelColor(0, rgb.Color(255, 255, 0)); rgb.show(); } // WiFi Connected/Waiting
void setStatusBlue()   { rgb.setPixelColor(0, rgb.Color(0, 0, 255));   rgb.show(); } // Raw Packet Received
void setStatusGreen()  { rgb.setPixelColor(0, rgb.Color(0, 255, 0));   rgb.show(); } // Playing Song
void setStatusPurple() { rgb.setPixelColor(0, rgb.Color(128, 0, 128)); rgb.show(); } // MP3 Error / Silence

void initGroveMP3() {
  delay(500);
  Serial1.print("AT+VOL=10\r\n");      
  delay(100);
  checkMP3Response();
  
  // Set the specific repeat mode that worked yesterday
  Serial1.print("AT+REPEATMODE=4\r\n");  
  delay(100);
  checkMP3Response();
}

void setup() {
  // Initialize USB Serial communication
  Serial.begin(115200); 
  
  // Initialize status LED immediately so we can track boot stages
  rgb.begin();
  rgb.setBrightness(70);
  setStatusRed(); // Solid Red = Booting up / Not connected
  
  // 1. Turn on UART pins for MP3
  Serial1.begin(115200, SERIAL_8N1, GROVE_RX_PIN, GROVE_TX_PIN);
  
  // 2. Let the power settle, then initialize the MP3 player
  delay(600); 
  initGroveMP3();
  delay(600); // Extra spacing delay to prevent concurrent Wi-Fi / UART noise spikes

  Serial.println("\n--- Initiating Wi-Fi Connection ---");

  bool connected = false;
  int retryAttempts = 0;

  while (!connected) {
    WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
    retryAttempts = 0;

    // Try connecting up to 10 times (5 seconds total) before giving up and resetting
    while (WiFi.status() != WL_CONNECTED && retryAttempts < 10) {
      delay(500);
      Serial.print(".");
      retryAttempts++;
    }

    if (WiFi.status() == WL_CONNECTED) {
      connected = true;
    } else {
      // If it failed, flash Red to signal a retry, disconnect, and loop back
      Serial.println("\nConnection timed out. Resetting Wi-Fi radio and retrying...");
      WiFi.disconnect(true); 
      delay(1000); 
    }
  }
  
  // Connection successful!
  Serial.println("\nWi-Fi Connected Successfully!");
  Serial.print("AtomS3U IP Address: ");
  Serial.println(WiFi.localIP());

  setStatusYellow(); // Turn Yellow = Safely connected and waiting for Wio Packets
  udp.begin(UDP_PORT);
}

void loop() {
  int packetSize = udp.parsePacket();
  
  if (packetSize) {
    setStatusBlue(); // Blue flash = Network packet hit
    
    char packetBuffer[255];
    int len = udp.read(packetBuffer, 255);
    if (len > 0) packetBuffer[len] = 0;

    String command = String(packetBuffer);
    Serial.print("Received UDP: ["); Serial.print(command); Serial.println("]");

    if (command == "STOP") {
      Serial1.print("AT+PLAY=0\r\n"); // Explicit CRLF
      Serial.println("Sent to MP3: STOP");
      checkMP3Response();
      delay(300);
      setStatusYellow(); 
    }
    else if (command.startsWith("PLAY:")) {
      String trackStr = command.substring(5); 
      int trackNumber = trackStr.toInt(); 
      
      playTrack(trackNumber); 
    }
  }
}

void playTrack(uint8_t trackNumber) {
  // Matches yesterday's exact working syntax: AT+PLAY=sd0,1
  String command = "AT+PLAY=sd0," + String(trackNumber) + "\r\n";
  Serial1.print(command); 
  
  Serial.print("Sent to MP3: ");
  Serial.print(command);

  delay(200); 
  bool success = checkMP3Response();
  
  if (success) {
    setStatusGreen(); 
  } else {
    setStatusPurple(); 
  }
}

// Reads UART response from the MP3 player
bool checkMP3Response() {
  String response = "";
  unsigned long startWait = millis();
  
  // Wait up to 100ms for a response
  while (millis() - startWait < 100) {
    while (Serial1.available()) {
      char c = Serial1.read();
      response += c;
    }
  }
  
  if (response.length() > 0) {
    Serial.print("MP3 Chip Response: [");
    Serial.print(response);
    Serial.println("]");
    
    // Most AT firmware returns "OK" on success. If it returns "ERROR", fail.
    if (response.indexOf("ERROR") >= 0) {
      return false;
    }
    return true; // Received a valid response (likely "OK")
  }
  
  Serial.println("MP3 Chip Response: SILENCE (No UART data returned)");
  return false; // Hardware silence
}

By admin

Leave a Reply

Your email address will not be published. Required fields are marked *