NFC music box: embed an NTAG215 in a printed token, read it with ESP32 + PN532
Seal an NTAG215 tag in a printed token with a mid-print pause; an ESP32 and PN532 read its UID and play that token's song. Pocket depth and pause maths included.
- Every NTAG215 has a factory-set 7-byte UID; use it as the token's ID without writing anything to the tag.
- Make the pocket as deep as the tag and pause at the first layer above it; the resumed print seals the tag inside.
- About 1–2 mm of plastic over the tag reads well. Test every tag before embedding: once sealed, it cannot come out.
- The PN532 has three interface modes. Set its switches to SPI before powering up, or the code won't find the reader.
An NFC music box has an NFC reader under its lid: put a 3D-printed token on top and the box plays that token’s song. The trick is inside the token. An NTAG215 tag is sealed in with a mid-print pause, every tag carries its own unique ID (UID), and an ESP32 with a PN532 reader just reads that ID and picks the song.
This is a how-to guide, not a write-up of a finished build. Sizes and numbers in the example are starting points; measure your actual tags before designing.
Parts
- Any ESP32 board with SPI (pins here are for the original ESP32)
- A PN532 reader module with mode switches
- NTAG215 tags, round sticker type
- A passive buzzer for melodies (or a sound module if you want real recordings)
- PLA or PETG for the tokens and box
About NTAG21x tags
NXP’s datasheet describes the family as NFC Forum Type 2 tags working at 13.56 MHz, with a factory-programmed 7-byte UID, 10-year data retention and 100,000 write cycles. The three models differ in memory:
| Model | User memory | Good for |
|---|---|---|
| NTAG213 | 144 bytes | Short links, or UID only |
| NTAG215 | 504 bytes | Widely used, reads with phones |
| NTAG216 | 888 bytes | Longer data such as JSON |
The OpenSpool project, which uses an ESP32 and a PN532 to read tags on filament spools, needs NTAG215/216 because it stores JSON on the tag. Our music box only uses the UID, so any model works. NTAG215 is easy to find and leaves room if you later want to write data to it.
Worked example: pocket and pause height
Always measure the tag with calipers first; size and thickness vary by maker. Say your round sticker measures 25 mm across and at most 0.4 mm thick: make the pocket 26 mm wide, 0.5 mm clearance all round.
The token is a 4 mm tall cylinder printed at 0.2 mm layers. Its base is the side that sits on the lid, so the tag goes near the base.
- Floor under the tag: 1.2 mm = layers 1 to 6 (6 × 0.2). This is the plastic between tag and reader, inside the 1–2 mm range printpal says reads well.
- Tag pocket: 0.4 mm deep = layers 7–8, finishing at 1.6 mm.
- Pause: before layer 9, which prints at 1.8 mm. (In PrusaSlicer, move the layer slider to 1.80 and choose Insert pause print; it pauses before the selected layer.)
- Printed over the tag: layers 9 to 20 = 12 layers × 0.2 = 2.4 mm, for 4 mm in total.
Why the pocket should match the tag’s thickness: if it is deeper, the first resumed layer has to bridge over air, sags and can tilt the tag. If it is shallower, the tag sticks up and the nozzle hits it. Change the layer height and you must redo all of this.
Test every tag on the reader before embedding it. A dead tag inside a token cannot be fixed; you print a new token.
Embedding the tag
- Scan the tag and note its UID (the code below prints it to Serial).
- Slice, and add the pause at the layer you calculated. In OrcaSlicer or Bambu Studio, use the add-pause control on the layer slider in Preview.
- Print until it pauses. Check the pocket is clean, with no stray strings.
- Lay the tag flat in the pocket, antenna side up. A dab of glue stops it shifting.
- Resume, and watch the first layer printed over the tag.
Wiring
Set the PN532’s switches to SPI before powering it (one module maker’s guide: switch 1 = 0, switch 2 = 1, but numbering can vary by board revision; check the silkscreen). Then wire:
| PN532 | ESP32 |
|---|---|
| VCC | 3V3 |
| GND | GND |
| SCK | GPIO18 |
| MISO | GPIO19 |
| MOSI | GPIO23 |
| NSS (SS) | GPIO5 |
The buzzer goes on GPIO25 and GND. If you use a speaker or a sound module with a magnet, keep it away from the reader: metal near the antenna interferes with reading. Test the range once everything is assembled.
Code: read the UID, play a melody
This uses the Adafruit PN532 library over hardware SPI. readPassiveTargetID takes a timeout, so the loop doesn’t block forever waiting for a tag.
#include <SPI.h>
#include <Adafruit_PN532.h>
const int PN532_SS = 5;
const int BUZZER = 25;
Adafruit_PN532 nfc(PN532_SS); // hardware SPI: SCK 18, MISO 19, MOSI 23
struct Note { int hz; int ms; };
const Note TWINKLE[] = {{262,400},{262,400},{392,400},{392,400},{440,400},{440,400},{392,800}};
const Note SCALE[] = {{262,250},{294,250},{330,250},{349,250},{392,250},{440,250},{494,250},{523,500}};
struct Token { uint8_t uid[7]; const Note* song; int len; };
Token tokens[] = { // put the real UIDs from Serial here
{{0x04,0x11,0x22,0x33,0x44,0x55,0x66}, TWINKLE, 7},
{{0x04,0xAA,0xBB,0xCC,0xDD,0xEE,0xFF}, SCALE, 8},
};
uint8_t lastUid[7];
unsigned long lastSeen = 0;
void play(const Note* s, int n) {
for (int i = 0; i < n; i++) { tone(BUZZER, s[i].hz, s[i].ms); delay(s[i].ms + 50); }
}
void setup() {
Serial.begin(115200);
nfc.begin();
if (!nfc.getFirmwareVersion()) { Serial.println("PN532 not found: check SPI switches"); while (1) delay(10); }
}
void loop() {
uint8_t uid[7]; uint8_t len;
if (nfc.readPassiveTargetID(PN532_MIFARE_ISO14443A, uid, &len, 200) && len == 7) {
bool same = memcmp(uid, lastUid, 7) == 0 && millis() - lastSeen < 1000;
lastSeen = millis();
if (same) return; // token still on the lid: don't replay
memcpy(lastUid, uid, 7);
for (int i = 0; i < 7; i++) Serial.printf("%02X ", uid[i]);
Serial.println();
for (auto& t : tokens)
if (memcmp(uid, t.uid, 7) == 0) { play(t.song, t.len); lastSeen = millis(); }
}
}
The frequencies are notes from C4 = 262 Hz up to C5 = 523 Hz. The first melody is the traditional “Twinkle Twinkle”; the second is a scale. Edit or add songs in the tokens table.
The box
Print the box so the reader sits right under the lid; the thinner the lid, the easier the read. Add a round recess in the lid so a token lands centred over the antenna. The board lives inside a closed case; see designing a case that fits. If you use a battery, read lithium battery safety first.
Common mistakes
- PN532 in the wrong mode. The code reports the reader missing. Power off, set the switches, power on.
- Pocket too deep. The covering layer bridges and sags; the tag tilts.
- Not testing tags before embedding. A dead or wrong-type tag is sealed in for good.
- Not recalculating the pause after changing layer height. The printer stops on the wrong layer.
- Too much plastic between tag and reader. The tag won’t read, or only when pressed flat. Keep the reader-facing side thin.
- No timeout on
readPassiveTargetID. The default waits forever, so the loop hangs until a tag appears.
Small tokens with a tag inside are not toys for young children who still put things in their mouths. Make tokens large enough and seal the tag in so it cannot be picked out.
Sources
- NTAG213/215/216 product data sheet — NXP
- How to use NFC tags in 3D prints — printpal
- OpenSpool (ESP32 + PN532 + NTAG215/216) — GitHub
- Adafruit-PN532 Arduino library (examples) — GitHub
- Getting started with PN532 for Arduino and ESP32 — Elechouse
- Insert pause or custom G-code at layer — Prusa Knowledge Base
FAQ
Can I use an NTAG213 or NTAG216 instead?
Yes, for this build, since we only read the UID, which all three have. They differ in memory (144, 504 and 888 bytes), which only matters if you also want to store data on the tag.
How far through the lid will it read?
NXP's datasheet gives an operating distance of up to 100 mm, depending on field strength and antenna geometry. Small modules and small tags usually manage much less. Keep the lid thin and measure the real range with your own parts.
Can I change songs without editing the code?
Yes, by writing data to the tag before embedding it, for example a song number as an NDEF record, and having the code read that page instead of the UID. Write and test it before you seal the tag in.
Circuits and code were tested on sample boards. Boards differ in pin layout, so check your board's pinout before you power anything.