edge lab LIVE! starts Tuesday in Munich. Grab a workshop seat and take home the board you build on. Code FRIEND-HACKSTER saves 20%. () Learn more!
Ivo Sirucek
Published © GPL3+

Smart light brick

A fun smart brick that can be added to regular building sets. It lights up and plays according to NFC tags.

AdvancedFull instructions provided20 hours23
Smart light brick

Story

Read more

Custom parts and enclosures

kostka_velka_upravena_9uyYVj11Wh.stl

dno_kostky_sgbUajpmLZ.3mf

Schematics

gerber_cube_pcb_cube_3_2026-10-11_k0O6pWdNIQ.zip

pcb_pcb_cube_3_2026-10-11_(1)_zZqqZqdlHd.pdf

schematic_cube_2026-10-11_c4z0DKeX6K.pdf

Code

Untitled file

C#
#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();
  }
}

Credits

Ivo Sirucek
3 projects • 0 followers
I’ve enjoyed tinkering since I was young. This led to studies in microelectronics. Today, I can develop complex programs for the ESP32.

Comments