What if a LEGO brick could recognise who just stepped on it? That question turned into the NFC Music Brick: a fully LEGO-compatible 2×4 brick [adjust size] that hides an ESP32, an NFC reader, smart LEDs, a piezo buzzer and its own rechargeable battery.
Put a tagged minifigure or brick on top and the Music Brick wakes up. It reads the tag, plays that character's tune and runs a matching light show. The knight gets a fanfare in gold, the astronaut gets a blue sci-fi beep sequence, the dragon gets a red flicker.
The key idea: the brick holds no fixed list of songs. Everything it plays is stored on the tag itself. Write a new tag with your phone and you have a new song and a new effect, with no reflashing and no app.
It is wireless and self-contained, so it works on any baseplate, on the carpet, or in a pocket. It charges from a normal USB cable.
Results and what's nextThe Music Brick reacts within a fraction of a second, and kids quickly start inventing their own tags.
Ways to play
• Give every minifigure its own theme song.
• Build a "door" that only opens with a fanfare for the right key-brick.
• Make a musical sequencer: a row of tagged bricks, each a few notes.
• Hide tags in a treasure hunt; the brick plays a hint when one is found.
Ideas for version 2
• Use the ESP32's Wi-Fi or Bluetooth so several Music Bricks play in sync.
• A small web page for composing songs and writing tags from the browser.
• A real speaker with an I2S amplifier for richer sound.
#include <stdint.h> // for uint8_t in forward enum declaration
// Forward declarations to satisfy Arduino IDE auto-generated function prototypes
enum State : uint8_t;
struct Scene;
/*
CubeToy – ESP32-C3 + RC522 (NTAG213 NDEF 2x Text) + SK6812/NeoPixel + Piezo
Low-power úpravy:
- CPU frekvence: IDLE/WAIT_REMOVE = 80 MHz, PLAYING = 160 MHz
- Skenování tagu v IDLE jen každých 100 ms (odezva <= 0.2 s)
Pozn.: toneObj stop() nepoužíváme (některé verze ho nemají).
*/
#include <Arduino.h>
#include <SPI.h>
#include <MFRC522.h>
#include <Adafruit_NeoPixel.h>
#include <toneObj.h>
#include <anyrtttl.h>
#include <binrtttl.h>
#include <pitches.h>
// ===== PINY (uprav podle svého zapojení) =====
static const uint8_t PIN_RC522_SCK = 20;
static const uint8_t PIN_RC522_MISO = 16;
static const uint8_t PIN_RC522_MOSI = 17;
static const uint8_t PIN_RC522_SS = 0;
static const uint8_t PIN_RC522_RST = 3;
static const uint8_t PIN_LED_DATA = 15;
static const uint8_t LED_COUNT = 14;
static const uint8_t PIEZO_PIN = 19;
// ===== LED limit =====
static const uint8_t BRIGHTNESS_LIMIT = 18;
// ===== Úspory =====
static const uint32_t SCAN_PERIOD_MS = 100; // <=0.2s odezva
static const uint32_t CPU_MHZ_IDLE = 80;
static const uint32_t CPU_MHZ_PLAY = 160;
// ===== NTAG213 user memory: pages 4..39 = 144 bytes =====
static const uint8_t NTAG_FIRST_PAGE = 4;
static const uint8_t NTAG_LAST_PAGE = 39;
static const uint16_t NTAG_USER_BYTES = 144;
// ---------- RTTTL SONGS (PROGMEM) ----------
const char SONG_Money[] PROGMEM = "Money:d=4,o=5,b=112:8e7,8e7,8e7,8e7,8e7,8e7,16e6,16a6,16c7,16e7,8d#7,8d#7,8d#7,8d#7,8d#7,8d#7,16f6,16a6,16c7,16d#7,d7,8c7,8a6,8c7,c7,2a6,32a6,32c7,32e7,8a7";
const char SONG_Bolero[] PROGMEM = "Bolero:d=16,o=5,b=80:c6,8c6,b,c6,d6,c6,b,a,8c6,c6,a,4c6,8c6,b,c6,a,g,e,f,2g,g,f,e,d,e,f,g,a,4g,4g,g,a,b,a,g,f,e,d,e,d,8c,8c,c,d,8e,8f,4d,2g";
const char SONG_StarWars1[] PROGMEM = "StarWars:d=4,o=5,b=80:8d.,8d.,8d.,8a#4,16f,8d.,8a#4,16f,d.,32p,8a.,8a.,8a.,8a#,16f,8c#.,8a#4,16f,d.,32p,8d.6,8d,16d,8d6,32p,8c#6,16c6,16b,16a#,8b,32p,16d#,8g#,32p,8g,16f#,16f,16e,8f,32p,16a#4,8c#,32p,8a#4,16c#,8f.,8d,16f,a.,32p,8d.6,8d,16d,8d6,32p,8c#6,16c6,16b,16a#,8b,32p,16d#,8g#,32p,8g,16f#,16f,16e,8f,32p,16a#4,8c#,32p,8a#4,16f,8d.,8a#4,16f,d.";
const char SONG_StarWars2[] PROGMEM = "StarWars2:d=4,o=5,b=180:8f,8f,8f,2a#.,2f.6,8d#6,8d6,8c6,2a#.6,f.6,8d#6,8d6,8c6,2a#.6,f.6,8d#6,8d6,8d#6,2c6,p,8f,8f,8f,2a#.,2f.6,8d#6,8d6,8c6,2a#.6,f.6,8d#6,8d6,8c6,2a#.6,f.6,8d#6,8d6,8d#6,2c6";
const char SONG_KnightRider[] PROGMEM = "Knight_Rider:d=32,o=5,b=63:16e,f,e,8b,16e6,f6,e6,8b,16e,f,e,16b,16e6,4d6,8p,4p,16e,f,e,8b,16e6,f6,e6,8b,16e,f,e,16b,16e6,4f6";
const char SONG_LaserGun[] PROGMEM = "LaserGun:d=32,o=6,b=250:c,e,g,b,c7";
const char SONG_NothingE[] PROGMEM = "NothingE:d=4,o=5,b=125:e,g6,b6,e7,b6,g6,e,g6,b6,e7,b6,g6,2e,b6,2b6,2b6,32p,b6,c6,b6,a6,b6,a6,e6,2e6,2e6,16p,c6,e6,e6,f#6,e6,e6,16p,e6,f#6,2g6,g6,g6,2a6,a6,a6,2e6,2g6,2b6,2e7,2b6,2g6,2e6";
const char SONG_PoliceAl[] PROGMEM = "PoliceAl:d=4,o=6,b=120:a5,f5,a5,f5,a5,f5,a5,f5,a5,f5,a5,f5,a5,f5,a5,f5,a5,f5,a5,f5,a5";
const char SONG_Popcorn[] PROGMEM = "Popcorn:d=16,o=5,b=160:a,p,g,p,a,p,e,p,c,p,e,p,8a4,8p,a,p,g,p,a,p,e,p,c,p,e,p,8a4,8p,a,p,b,p,c6,p,b,p,c6,p,a,p,b,p,a,p,b,p,g,p,a,p,g,p,a,p,f,8a,8p,a,p,g,p,a,p,e,p,c,p,e,p,8a4,8p,a,p,g,p,a,p,e,p,c,p,e,p,8a4,8p,a,p,b,p,c6,p,b,p,c6,p,a,p,b,p,a,p,b,p,g,p,a,p,g,p,a,p,b,4c6";
const char SONG_R2D2Talk[] PROGMEM = "R2D2_Talk:d=16,o=6,b=200:32g7,32p,16a#6,16b6,16d7,16g6,32p,16e7,16d7,16b6,16a6,32p,32g7,32a#7,32g7,32e7,32p,16d7,16b6,16g6,32p,16e7,16g7,16a#7,8g7,32p,16d7,16b6,16g6,8p";
const char SONG_Amerika[] PROGMEM = "Amerika:d=4,o=5,b=125:d,d,8d,8d,8d,8f,a,8a,a.,8p,8a,g,8g,g.,8p,8g,g,8e,2e,8p,d,d,8d,8d,8d,8f,a,8a,a.,8p,8c6,c6,8e,e.,8p,8c6,c6,8e,e.,p,d,d,8d,8d,8d,8f,a,8a,a.,8p,8a,g,8g,g.,8p,8g,g,8e,2e,8p,d,d,8d,8d,8d,8f,a,8a,a.,8p,8c6,c6,8e,e.,8p,8c6,c6,8e,e";
const char SONG_PoliceSi[] PROGMEM = "PoliceSi:d=2,o=5,b=160:g,c6,g,c6,g,c6,g,c6,g,c6,g,c6,g,c6,g,c6,g,c6,g,c6,g,c6,g,c6";
const char SONG_Rolnicky[] PROGMEM = "Rolnicky:d=4,o=5,b=170:b,b,b,p,b,b,b,b,p,b,d6,g.,8a,2b.,8p,c6,c6,c6.,8c6,c6,b,b,8b,8b,b,a,a,b,2a,2d6";
const char SONG_Simpsonovi[] PROGMEM = "Simpsonovi:d=8,o=5,b=160:c.6,4e6,4f#6,a6,4g.6,4e6,4c6,a,f#,f#,f#,2g,p,p,f#,f#,f#,f#,g,4a#.,c6,c6,c6,4c6";
const char SONG_Communic[] PROGMEM = "Communic:d=4,o=5,b=100:16g#7,8a#7,16a#7,16a#7,8f#7";
const char SONG_CocaCola[] PROGMEM = "CocaCola:d=16,o=5,b=125:8f#7,8f#7,8f#7,8f#7,4g7,8f#7,4e7,8e7,8a7,4f#7,4d7,2p";
const char SONG_HarryPot[] PROGMEM = "HarryPot:d=16,o=5,b=125:2p,8p,8b,8e.6,g6,8f#6,4e6,8b6,4a.6,4f#.6,8e.6,g6,8f#6,4d6,8f6,2b,8p,8b,8e.6,g6,8f#6,4e6,8b6,4d7,8c#7,4c7,8g#6,8c.7,b6,8a#6,4f#6,8g6,2e6,8p,8g6,4b6,8g6,4b6,8g6,4c7,8b6,4a#6,8f#6,8g.6,b6,8a#6,4a#,8b,2b6,8p";
const char SONG_IndianaJ[] PROGMEM = "IndianaJ:d=4,o=5,b=250:32p,e5,8p,8f5,8g5,8p,1c6,8p,d5,8p,8e5,1f5,p,g5,8p,8a5,8b5,8p,1f6,p,a5,8p,8b5,2c6,2d6,2e6,e5,8p,8f5,8g5,8p,1c6,p,d6,8p,8e6,1f7";
const char SONG_MissionI[] PROGMEM = "MissionI:d=16,o=5,b=225:8g6,4p,8g6,4p,8f6,8p,8f#6,8p,8g6,4p,8g6,4p,8a#6,8p,8c7,8p,8g6,4p,8g6,4p,8f6,8p,8f#6,8p,8g6,4p,8g6,4p,8a#6,8p,8c7,8p,8a#6,8g6,1d6,p,8a#6,8g6,1c#6,p,8a#6,8g6,1c6,8p,8a#,8c6";
const char SONG_StarTrek[] PROGMEM = "StarTrek:d=32,o=6,b=63:8f#.5,b.5,d#.,8c#.,a.5,g#.,4f#.,a.5,e.,f#.,g#.,e.,8d#,8c#,8b.5,d#.,b.5,2c#.";
const char SONG_XFiles[] PROGMEM = "X-Files:d=16,o=5,b=125:4e,4b,4a,4b,4d6,2b,1p,4e,4b,4a,4b,4e6,2b,1p,4g6,4f#6,4e6,4d6,4e6,2b,1p,4g6,4f#6,4e6,4d6,4f#6,2b,1p,4e,4b,4a,4b,4d6,2b,1p,4e,4b,4a,4b,4e6,2b,1p,4e6,2b";
const char SONG_HappyBir[] PROGMEM = "HappyBir:d=4,o=5,b=100:16c#6,16c#6,8d#6,8c#6,8f#6,8e#6,8p,16c#6,16c#6,8d#6,8c#6,8g#6,8f#6,16p,16c#6,8c#7,8a#6,8f#6,8e#6,8d#6,16p,16a#6,16a#6,8a#6,8f#6,8g#6";
// ---------- LED profily ----------
enum LedMode : uint8_t {
LEDM_MONEY,
LEDM_BOLERO,
LEDM_STARWARS,
LEDM_KNIGHTRIDER,
LEDM_LASERGUN,
LEDM_R2D2,
LEDM_POLICE,
LEDM_POPCORN,
LEDM_AMERIKA,
LEDM_ROLNICKY,
LEDM_SIMPSON,
LEDM_COMMUNIC,
LEDM_COCACOLA,
LEDM_HARRYPOT,
LEDM_INDIANA,
LEDM_MISSION,
LEDM_STARTREK,
LEDM_XFILES,
LEDM_HAPPYB
};
struct LedProfile {
LedMode mode;
uint8_t brightness;
uint32_t c1;
uint32_t c2;
};
struct Scene {
const char* code; // krátký kód na tagu (Text record)
const char* songProgmem; // RTTTL (PROGMEM)
LedProfile led;
};
MFRC522 mfrc522(PIN_RC522_SS, PIN_RC522_RST);
Adafruit_NeoPixel pixels(LED_COUNT, PIN_LED_DATA, NEO_GRB + NEO_KHZ800);
toneObj piezo(PIEZO_PIN);
#define RGB(r,g,b) pixels.Color((uint8_t)(r),(uint8_t)(g),(uint8_t)(b))
const Scene SCENES[] = {
{"MNY", SONG_Money, { LEDM_MONEY, BRIGHTNESS_LIMIT, RGB(0,180,40), RGB(255,180,0) } },
{"BOL", SONG_Bolero, { LEDM_BOLERO, BRIGHTNESS_LIMIT, RGB(255,60,0), RGB(255,140,0) } },
{"SW1", SONG_StarWars1, { LEDM_STARWARS, BRIGHTNESS_LIMIT, RGB(0,80,255), RGB(255,20,20) } },
{"SW2", SONG_StarWars2, { LEDM_STARWARS, BRIGHTNESS_LIMIT, RGB(0,80,255), RGB(255,20,20) } },
{"KNR", SONG_KnightRider, { LEDM_KNIGHTRIDER, BRIGHTNESS_LIMIT, RGB(255,0,0), RGB(30,0,0) } },
{"LSR", SONG_LaserGun, { LEDM_LASERGUN, BRIGHTNESS_LIMIT, RGB(200,255,255), RGB(20,80,255) } },
{"NTG", SONG_NothingE, { LEDM_COMMUNIC, BRIGHTNESS_LIMIT, RGB(255,0,200), RGB(0,160,255) } },
{"PAL", SONG_PoliceAl, { LEDM_POLICE, BRIGHTNESS_LIMIT, RGB(255,0,0), RGB(0,0,255) } },
{"POP", SONG_Popcorn, { LEDM_POPCORN, BRIGHTNESS_LIMIT, RGB(255,220,0), RGB(255,255,255) } },
{"R2D", SONG_R2D2Talk, { LEDM_R2D2, BRIGHTNESS_LIMIT, RGB(0,200,255), RGB(255,255,255) } },
{"AME", SONG_Amerika, { LEDM_AMERIKA, BRIGHTNESS_LIMIT, RGB(255,0,0), RGB(0,0,255) } },
{"PSI", SONG_PoliceSi, { LEDM_POLICE, BRIGHTNESS_LIMIT, RGB(255,0,0), RGB(0,0,255) } },
{"ROL", SONG_Rolnicky, { LEDM_ROLNICKY, BRIGHTNESS_LIMIT, RGB(255,180,40), RGB(255,255,255) } },
{"SIM", SONG_Simpsonovi, { LEDM_SIMPSON, BRIGHTNESS_LIMIT, RGB(255,200,0), RGB(255,80,0) } },
{"COM", SONG_Communic, { LEDM_COMMUNIC, BRIGHTNESS_LIMIT, RGB(255,0,200), RGB(0,160,255) } },
{"COL", SONG_CocaCola, { LEDM_COCACOLA, BRIGHTNESS_LIMIT, RGB(255,0,0), RGB(255,255,255) } },
{"HPT", SONG_HarryPot, { LEDM_HARRYPOT, BRIGHTNESS_LIMIT, RGB(130,0,255), RGB(255,200,60) } },
{"IND", SONG_IndianaJ, { LEDM_INDIANA, BRIGHTNESS_LIMIT, RGB(255,140,0), RGB(120,40,0) } },
{"MIS", SONG_MissionI, { LEDM_MISSION, BRIGHTNESS_LIMIT, RGB(0,255,180), RGB(255,255,255) } },
{"STT", SONG_StarTrek, { LEDM_STARTREK, BRIGHTNESS_LIMIT, RGB(0,160,255), RGB(0,255,255) } },
{"XFL", SONG_XFiles, { LEDM_XFILES, BRIGHTNESS_LIMIT, RGB(0,255,60), RGB(0,40,0) } },
{"HBD", SONG_HappyBir, { LEDM_HAPPYB, BRIGHTNESS_LIMIT, RGB(255,255,255), RGB(255,255,255) } },
};
static const uint8_t SCENE_COUNT = sizeof(SCENES)/sizeof(SCENES[0]);
const Scene* findScene(const String& code) {
String c = code; c.trim(); c.toUpperCase();
for (uint8_t i=0;i<SCENE_COUNT;i++) {
if (c == SCENES[i].code) return &SCENES[i];
}
return nullptr;
}
// ---------- Sdílený stav mezi audio a LED (note-driven) ----------
static const Scene* gScene = nullptr;
static uint16_t gStep = 0;
static unsigned long gNoteStartMs = 0;
static unsigned long gNoteDurMs = 1;
static unsigned int gFreq = 0;
static bool gIsRest = true;
static void onNoteEvent(unsigned int freq, unsigned long durMs) {
gFreq = freq;
gNoteStartMs = millis();
gNoteDurMs = (durMs == 0 ? 1 : durMs);
gIsRest = (freq == 0);
gStep++;
}
static uint8_t easeInOut(uint8_t x) {
uint16_t t = x;
uint16_t y = (t * t) / 255;
return (uint8_t)((y * (510 - t)) / 255);
}
static void fillAll(uint32_t c) {
for (int i=0;i<LED_COUNT;i++) pixels.setPixelColor(i, c);
}
static uint32_t dimColor(uint32_t c, uint8_t dim) {
uint8_t r = (c >> 16) & 0xFF;
uint8_t g = (c >> 8) & 0xFF;
uint8_t b = c & 0xFF;
r = (uint8_t)((uint16_t)r * dim / 255);
g = (uint8_t)((uint16_t)g * dim / 255);
b = (uint8_t)((uint16_t)b * dim / 255);
return RGB(r,g,b);
}
static void scanner(uint32_t c, uint8_t pos, uint8_t tail) {
fillAll(0);
for (int t=0; t<tail; t++) {
int p = (int)pos - t;
if (p >= 0 && p < LED_COUNT) {
uint8_t dim = 255 - (t * (255 / tail));
pixels.setPixelColor(p, dimColor(c, dim));
}
}
}
static uint32_t wheel(byte pos) {
pos = 255 - pos;
if (pos < 85) return RGB(255 - pos * 3, 0, pos * 3);
if (pos < 170) { pos -= 85; return RGB(0, pos * 3, 255 - pos * 3); }
pos -= 170; return RGB(pos * 3, 255 - pos * 3, 0);
}
static void renderLedTick() {
pixels.setBrightness(BRIGHTNESS_LIMIT);
if (!gScene || gIsRest) {
fillAll(0);
pixels.show();
return;
}
unsigned long now = millis();
unsigned long dt = now - gNoteStartMs;
uint8_t phase = (dt >= gNoteDurMs) ? 255 : (uint8_t)((dt * 255UL) / gNoteDurMs);
uint8_t pulse = easeInOut(phase);
bool high = (gFreq >= 900);
bool mid = (gFreq >= 450 && gFreq < 900);
switch (gScene->led.mode) {
case LEDM_MONEY: {
fillAll(gScene->led.c1);
if ((gStep % 4) == 0) pixels.setPixelColor(random(0, LED_COUNT), gScene->led.c2);
pixels.setPixelColor(gStep % LED_COUNT, RGB(pulse/2, pulse/2, pulse/2));
} break;
case LEDM_BOLERO: {
uint8_t active = (uint8_t)min<int>(LED_COUNT, 1 + (gStep / 8));
fillAll(0);
for (int i=0;i<active;i++) pixels.setPixelColor(i, (i%2==0) ? gScene->led.c1 : gScene->led.c2);
if (active>0) pixels.setPixelColor(active-1, RGB(pulse, pulse/3, 0));
} break;
case LEDM_STARWARS: {
bool redSide = ((gStep / 8) % 2) == 1;
uint32_t saber = redSide ? gScene->led.c2 : gScene->led.c1;
uint8_t pos = (uint8_t)(gStep % LED_COUNT);
scanner(saber, pos, 6);
if (high) pixels.setPixelColor(pos, RGB(255, 220, 60));
} break;
case LEDM_KNIGHTRIDER: {
uint16_t span = (LED_COUNT - 1) * 2;
uint16_t p = gStep % span;
uint8_t pos = (p < (LED_COUNT-1)) ? p : (span - p);
scanner(gScene->led.c1, pos, 8);
} break;
case LEDM_LASERGUN: {
fillAll(0);
int pos = random(0, LED_COUNT);
pixels.setPixelColor(pos, gScene->led.c1);
if (pos+1 < LED_COUNT) pixels.setPixelColor(pos+1, gScene->led.c2);
if (pos-1 >= 0) pixels.setPixelColor(pos-1, gScene->led.c2);
} break;
case LEDM_R2D2: {
fillAll(0);
int sparks = high ? 5 : (mid ? 3 : 2);
for (int s=0;s<sparks;s++) pixels.setPixelColor(random(0, LED_COUNT), gScene->led.c1);
if (high) pixels.setPixelColor(random(0, LED_COUNT), gScene->led.c2);
} break;
case LEDM_POLICE: {
bool red = (gStep % 2) == 0;
fillAll(red ? gScene->led.c1 : gScene->led.c2);
if ((gStep % 4) == 0) fillAll(RGB(255,255,255));
} break;
case LEDM_POPCORN: {
fillAll(0);
for (int i=0;i<4;i++) pixels.setPixelColor(random(0, LED_COUNT), (random(0,2)==0)?gScene->led.c1:gScene->led.c2);
} break;
case LEDM_AMERIKA: {
for (int i=0;i<LED_COUNT;i++) {
int m = (i + gStep) % 3;
pixels.setPixelColor(i, m==0 ? RGB(255,0,0) : (m==1 ? RGB(255,255,255) : RGB(0,0,255)));
}
} break;
case LEDM_ROLNICKY: {
fillAll(0);
for (int i=0;i<4;i++) pixels.setPixelColor(random(0, LED_COUNT), (random(0,3)==0)?gScene->led.c2:gScene->led.c1);
} break;
case LEDM_SIMPSON: {
fillAll(gScene->led.c1);
pixels.setPixelColor(gStep % LED_COUNT, gScene->led.c2);
} break;
case LEDM_COMMUNIC: {
bool a = (gStep % 2) == 0;
fillAll(a ? gScene->led.c1 : gScene->led.c2);
pixels.setPixelColor(random(0,LED_COUNT), RGB(pulse, pulse, pulse));
} break;
case LEDM_COCACOLA: {
fillAll(gScene->led.c1);
pixels.setPixelColor(random(0,LED_COUNT), gScene->led.c2);
if (high) pixels.setPixelColor(random(0,LED_COUNT), gScene->led.c2);
} break;
case LEDM_HARRYPOT: {
fillAll(gScene->led.c1);
int s = high ? 4 : 2;
for (int i=0;i<s;i++) pixels.setPixelColor(random(0,LED_COUNT), gScene->led.c2);
} break;
case LEDM_INDIANA: {
for (int i=0;i<LED_COUNT;i++) {
uint8_t v = (uint8_t)((i*20 + gStep*8) & 255);
pixels.setPixelColor(i, RGB(255, v/2, 0));
}
} break;
case LEDM_MISSION: {
fillAll(gScene->led.c1);
if (high) fillAll(gScene->led.c2);
} break;
case LEDM_STARTREK: {
for (int i=0;i<LED_COUNT;i++) {
uint8_t v = (uint8_t)((i*16 + gStep*12) & 255);
pixels.setPixelColor(i, (v<128) ? gScene->led.c1 : gScene->led.c2);
}
} break;
case LEDM_XFILES: {
fillAll(gScene->led.c2);
pixels.setPixelColor(random(0,LED_COUNT), gScene->led.c1);
} break;
case LEDM_HAPPYB: {
fillAll(0);
for (int i=0;i<6;i++) pixels.setPixelColor(random(0,LED_COUNT), wheel((byte)random(0,255)));
if ((gStep % 8)==0) fillAll(RGB(255,255,255));
} break;
}
pixels.show();
}
// ---------- AnyRtttl backend přes toneObj (NEBLOKUJÍCÍ) ----------
static void myTone(uint8_t /*pin*/, unsigned int freq, unsigned long durMs) {
onNoteEvent(freq, durMs);
if (freq == 0) return;
piezo.play(freq, durMs);
}
static void myNoTone(uint8_t /*pin*/) { }
static unsigned long myMillis() { return millis(); }
// ---------- NDEF parser: vytáhne 2 Text recordy ----------
// ---------- NDEF parser: vytáhne 2 Text recordy ----------
// TLV-aware: některé tagy mají před NDEF i jiné TLV (např. Lock Control TLV 0x01).
// Proto projdeme TLV strukturu a najdeme NDEF Message TLV (0x03) na hranici TLV.
bool parseNdefTwoTexts(const uint8_t* buf, size_t len, String &out1, String &out2) {
out1 = "";
out2 = "";
// 1) Najdi NDEF Message TLV (0x03) průchodem TLV
size_t i = 0;
const uint8_t* ndefPtr = nullptr;
size_t ndefLen = 0;
while (i < len) {
uint8_t tlvType = buf[i++];
if (tlvType == 0x00) continue; // NULL TLV
if (tlvType == 0xFE) break; // Terminator TLV
if (i >= len) break;
size_t tlvLen = 0;
if (buf[i] == 0xFF) {
if (i + 2 >= len) break;
tlvLen = ((size_t)buf[i + 1] << 8) | (size_t)buf[i + 2];
i += 3;
} else {
tlvLen = buf[i++];
}
if (i + tlvLen > len) {
// nekonzistence – ořízni
tlvLen = (len > i) ? (len - i) : 0;
}
if (tlvType == 0x03) {
ndefPtr = &buf[i];
ndefLen = tlvLen;
break;
}
i += tlvLen; // přeskoč hodnotu TLV
}
if (!ndefPtr || ndefLen == 0) return false;
// 2) Parse NDEF recordů – vytáhni 1–2 Text recordy
size_t p = 0;
int found = 0;
while (p < ndefLen) {
if (p + 2 > ndefLen) break;
uint8_t hdr = ndefPtr[p++];
bool sr = hdr & 0x10;
bool il = hdr & 0x08;
uint8_t tnf = hdr & 0x07;
uint8_t typeLen = ndefPtr[p++];
uint32_t payloadLen = 0;
if (sr) {
if (p >= ndefLen) break;
payloadLen = ndefPtr[p++];
} else {
if (p + 3 >= ndefLen) break;
payloadLen = ((uint32_t)ndefPtr[p] << 24) | ((uint32_t)ndefPtr[p + 1] << 16) | ((uint32_t)ndefPtr[p + 2] << 8) | ndefPtr[p + 3];
p += 4;
}
uint8_t idLen = 0;
if (il) {
if (p >= ndefLen) break;
idLen = ndefPtr[p++];
}
if (p + typeLen + idLen + payloadLen > ndefLen) break;
const uint8_t* typePtr = &ndefPtr[p];
p += typeLen;
p += idLen;
const uint8_t* payloadPtr = &ndefPtr[p];
p += payloadLen;
if (tnf == 0x01 && typeLen == 1 && typePtr[0] == 'T') {
if (payloadLen < 1) continue;
uint8_t status = payloadPtr[0];
uint8_t langLen = status & 0x3F;
if (1 + langLen > payloadLen) continue;
const uint8_t* textPtr = payloadPtr + 1 + langLen;
size_t textLen = payloadLen - (1 + langLen);
String text;
for (size_t k = 0; k < textLen; k++) text += (char)textPtr[k];
if (found == 0) out1 = text;
else if (found == 1) out2 = text;
found++;
if (found >= 2) return true;
}
}
return (out1.length() > 0);
}
// ---------- Čtení NTAG213 user memory (pages 4..39) ----------
bool readNTAG213UserMemory(uint8_t* out, size_t outSize) {
if (outSize < NTAG_USER_BYTES) return false;
byte buffer[18];
byte size = sizeof(buffer);
size_t offset = 0;
for (uint8_t page = NTAG_FIRST_PAGE; page <= NTAG_LAST_PAGE; page += 4) {
size = sizeof(buffer);
MFRC522::StatusCode status = MFRC522::STATUS_ERROR;
for (uint8_t tries = 0; tries < 3; tries++) {
size = sizeof(buffer);
status = mfrc522.MIFARE_Read(page, buffer, &size);
if (status == MFRC522::STATUS_OK) break;
delay(5);
}
if (status != MFRC522::STATUS_OK) return false;
for (int j=0;j<16;j++) {
if (offset < NTAG_USER_BYTES) out[offset++] = buffer[j];
}
}
return true;
}
// ---------- Spuštění scény ----------
static void startScene(const Scene* sc) {
gScene = sc;
gStep = 0;
onNoteEvent(0, 1);
anyrtttl::nonblocking::begin(PIEZO_PIN, FPSTR(sc->songProgmem));
}
// ---------- Anti-repeat: čekej, dokud tag nezmizí ----------
enum State : uint8_t { ST_IDLE, ST_PLAY1, ST_PLAY2, ST_WAIT_REMOVE };
// ---------- Úspora: přepínání CPU frekvence podle stavu ----------
static void setCpuForState(State s) {
#if defined(ARDUINO_ARCH_ESP32)
static State last = (State)255;
if (s == last) return;
last = s;
if (s == ST_IDLE || s == ST_WAIT_REMOVE) {
setCpuFrequencyMhz((int)CPU_MHZ_IDLE);
} else {
setCpuFrequencyMhz((int)CPU_MHZ_PLAY);
}
#endif
}
static State state = ST_IDLE;
static String code1, code2;
static const Scene* sc1 = nullptr;
static const Scene* sc2 = nullptr;
static uint8_t lastUid[10];
static uint8_t lastUidLen = 0;
static uint8_t missCount = 0;
static unsigned long lastPresenceCheck = 0;
static void storeUid(const MFRC522::Uid& uid) {
lastUidLen = uid.size;
for (byte i=0;i<uid.size;i++) lastUid[i] = uid.uidByte[i];
}
static bool uidEquals(const MFRC522::Uid& uid) {
if (uid.size != lastUidLen) return false;
for (byte i=0;i<uid.size;i++) if (uid.uidByte[i] != lastUid[i]) return false;
return true;
}
static void tickWaitRemove() {
if (millis() - lastPresenceCheck < 50) return;
lastPresenceCheck = millis();
bool seen = false;
if (mfrc522.PICC_IsNewCardPresent() && mfrc522.PICC_ReadCardSerial()) {
if (uidEquals(mfrc522.uid)) seen = true;
mfrc522.PICC_HaltA();
mfrc522.PCD_StopCrypto1();
}
if (seen) {
missCount = 0;
} else {
if (missCount < 255) missCount++;
if (missCount >= 10) {
state = ST_IDLE;
gScene = nullptr;
missCount = 0;
}
}
}
void setup() {
Serial.begin(115200);
delay(200);
pixels.begin();
pixels.setBrightness(BRIGHTNESS_LIMIT);
fillAll(0);
pixels.show();
SPI.begin(PIN_RC522_SCK, PIN_RC522_MISO, PIN_RC522_MOSI, PIN_RC522_SS);
mfrc522.PCD_Init();
// Max Rx gain pro lepší čtení tagů
mfrc522.PCD_SetAntennaGain(mfrc522.RxGain_max);
mfrc522.PCD_AntennaOn();
randomSeed(esp_random());
anyrtttl::setToneFunction(&myTone);
anyrtttl::setNoToneFunction(&myNoTone);
anyrtttl::setMillisFunction(&myMillis);
Serial.println("CubeToy ready.");
}
void loop() {
// úspora: nastav CPU frekvenci jen při změně stavu
setCpuForState(state);
// Audio tick (non-blocking)
anyrtttl::nonblocking::play();
// LED tick: při přehrávání ~25 fps, jinak jen občas (LED jsou off)
static unsigned long lastLed = 0;
const bool playing = (state == ST_PLAY1 || state == ST_PLAY2);
const uint32_t ledPeriod = playing ? 40 : 500;
if (millis() - lastLed > ledPeriod) {
lastLed = millis();
renderLedTick();
}
if (state == ST_IDLE) {
static unsigned long lastScan = 0;
if (millis() - lastScan < SCAN_PERIOD_MS) return;
lastScan = millis();
if (!mfrc522.PICC_IsNewCardPresent()) return;
if (!mfrc522.PICC_ReadCardSerial()) return;
storeUid(mfrc522.uid);
uint8_t mem[NTAG_USER_BYTES];
bool ok = readNTAG213UserMemory(mem, sizeof(mem));
String t1, t2;
if (ok && parseNdefTwoTexts(mem, sizeof(mem), t1, t2)) {
code1 = t1; code2 = t2;
sc1 = findScene(code1);
sc2 = (code2.length() > 0) ? findScene(code2) : nullptr;
Serial.print("SEQ1="); Serial.print(code1);
Serial.print(" SEQ2="); Serial.println(code2);
if (sc1) {
startScene(sc1);
state = ST_PLAY1;
} else {
gScene = nullptr;
anyrtttl::nonblocking::stop();
state = ST_WAIT_REMOVE;
}
} else {
Serial.println("NDEF Text not found / read failed");
state = ST_WAIT_REMOVE;
}
mfrc522.PICC_HaltA();
mfrc522.PCD_StopCrypto1();
}
else if (state == ST_PLAY1) {
if (!anyrtttl::nonblocking::isPlaying()) {
if (sc2) {
startScene(sc2);
state = ST_PLAY2;
} else {
state = ST_WAIT_REMOVE;
}
}
}
else if (state == ST_PLAY2) {
if (!anyrtttl::nonblocking::isPlaying()) state = ST_WAIT_REMOVE;
}
else if (state == ST_WAIT_REMOVE) {
gScene = nullptr;
tickWaitRemove();
}
}


Comments