/*
ESP32-S3 (WROOM-1-N16R8) — STABLE SMOOTH STICKS + LittleFS + Audio + 2x74HC595
- TFT_eSPI (ILI9488)
- LittleFS label: "littlefs"
- RAW565 screens from LittleFS
- MAX98357 I2S plays /go.wav (scheduled 1.5s after press)
- Falling sticks idle screen with PSRAM background cache
- NO sprites, NO dynamic alloc per frame -> avoids trails/inverted artefacts on ILI9488 SPI
*/
#include "esp_heap_caps.h"
/* ===== ESP8266Audio (works on ESP32-S3) ===== */
#include "AudioFileSourceFS.h"
#include "AudioGeneratorWAV.h"
#include "AudioOutputI2S.h"
/* ================= PINS ================= */
#define BUTTON_START 21 // INPUT_PULLUP -> press to GND
// 74HC595 control (SAFE pins)
#define SR_DATA 15
#define SR_CLOCK 16
#define SR_LATCH 17
// I2S pins (SAFE pins)
#define I2S_BCLK 4
#define I2S_LRCLK 5
#define I2S_DIN 6
/* ================= CONSTANTS ================= */
#define SCREEN_WIDTH 320
#define SCREEN_HEIGHT 480
#define CHUNK_H 8
// Idle sticks
#define NUM_STICKS 10
#define STRIP_W 8
#define MAX_X_SLOTS (SCREEN_WIDTH / STRIP_W)
#define VISIBLE_Y 106
#define REVEAL_CLEAR_HEIGHT 8
// Stick look
#define HEAD_H 6
#define TAIL_H 6
#define MIN_STICK_H 40
#define MAX_STICK_H 120
// Smoothness (fixed frame pacing)
#define TARGET_FPS 50
#define FRAME_US (1000000UL / TARGET_FPS)
// Outputs
#define NUM_FIRES 12
#define NUM_LEDS 4
#define CHANNEL_ON_IS_HIGH 1
TFT_eSPI tft;
/* ================= AUDIO ================= */
AudioGeneratorWAV *wav = nullptr;
AudioFileSourceFS *wavFile = nullptr;
AudioOutputI2S *i2sOut = nullptr;
TaskHandle_t audioTaskHandle = nullptr;
unsigned long audioStartTime = 0;
bool audioScheduled = false;
void audioInit() {
i2sOut = new AudioOutputI2S();
i2sOut->SetPinout(I2S_BCLK, I2S_LRCLK, I2S_DIN);
i2sOut->SetGain(0.70f);
wav = new AudioGeneratorWAV();
}
inline void audioPump() {
if (wav && wav->isRunning()) {
if (!wav->loop()) {
wav->stop();
if (wavFile) { delete wavFile; wavFile = nullptr; }
}
}
}
void audioTask(void *param) {
for (;;) {
audioPump();
vTaskDelay(1);
}
}
void playWavOnce(const char *path) {
if (!wav || !i2sOut) return;
if (wav->isRunning()) {
wav->stop();
if (wavFile) { delete wavFile; wavFile = nullptr; }
}
wavFile = new AudioFileSourceFS(LittleFS, path);
wav->begin(wavFile, i2sOut);
}
inline void serviceScheduledAudio() {
if (audioScheduled && (int32_t)(millis() - audioStartTime) >= 0) {
playWavOnce("/go.wav");
audioScheduled = false;
}
}
/* ================= SHIFT REGISTERS (2x74HC595) ================= */
static uint16_t srState = 0;
void srWrite16(uint16_t value) {
#if CHANNEL_ON_IS_HIGH
uint16_t v = value;
#else
uint16_t v = ~value;
#endif
digitalWrite(SR_LATCH, LOW);
shiftOut(SR_DATA, SR_CLOCK, MSBFIRST, (uint8_t)(v >> 8)); // U2
shiftOut(SR_DATA, SR_CLOCK, MSBFIRST, (uint8_t)(v & 0xFF)); // U1
digitalWrite(SR_LATCH, HIGH);
}
inline void setAllOff() {
srState = 0;
srWrite16(srState);
}
inline void setFire(uint8_t ch, bool on) {
if (ch 12) return;
uint8_t bit = (ch 4) return;
uint8_t bit = 12 + (led - 1);
if (on) srState |= (1u 0) {
if (wait > 1500) delayMicroseconds((uint32_t)wait - 1000);
while ((int32_t)(nextFrameUs - micros()) > 0) {}
} else {
if (wait SCREEN_HEIGHT) h = SCREEN_HEIGHT - y;
if (h 0) tft.fillRect(x, top + headH, STRIP_W, bodyH, TFT_BLUE);
if (tailH > 0) tft.fillRect(x, bot - tailH, STRIP_W, tailH, TFT_YELLOW);
}
void drawIdleSticks() {
frameWait();
tft.startWrite();
// refresh reveal band
//tft.pushImage(0, VISIBLE_Y, SCREEN_WIDTH, REVEAL_CLEAR_HEIGHT, revealBG);
for (int i = 0; i SCREEN_HEIGHT + 60) {
sticks[i].y = (float)random(-200, -40);
sticks[i].h = random(MIN_STICK_H, MAX_STICK_H + 1);
sticks[i].vy = random(30, 55) / 10.0f;
sticks[i].lastY = (int)sticks[i].y;
continue;
}
// draw new
drawStick(sticks[i].x, y, sticks[i].h);
}
tft.endWrite();
serviceScheduledAudio();
yield();
}
/* ================= FIRE SEQUENCE ================= */
void runFireSequence() {
setAllOff();
int order[NUM_FIRES];
for (int i = 0; i 0; i--) {
int j = random(0, i + 1);
int t = order[i]; order[i] = order[j]; order[j] = t;
}
for (int i = 0; i isRunning()) {
delay(10);
yield();
}
}