Design record
Product architecture, site architecture, and the four assumptions that do not survive contact with the datasheets. Written before any parts are bought, which is the only time writing it is worth anything.
Contents
- The brief, and what “done” means
- The landscape: what exists and what it costs
- Four corrections — the card holds no audio · RFID is the wrong radio · Arduino cannot, and Bluetooth probably should not · no origin can do the whole job · WiFi, not Bluetooth · the whole flow
- Product architecture
- Component selection → (chosen: the parts, the losers, the power budget)
- The interaction model
- Site architecture
- Open questions for component selection
- Decision log
- Sources
1. The brief, and what “done” means
A four-year-old has discovered books on tape. The cassette workflow it replaces was good in a way the modern products are not: the physical object was the content, you could hand it to a child, and a grandparent could make one.
Done, for version one: a child can take a card off a shelf, put it on a box, and hear a person they know reading a book; take the card off and it stops; put it back and it carries on. A parent can record a new book on a phone in under five minutes without reading any documentation, and turn it into a card without buying anything but the card.
Everything else — battery, buttons, enclosure, multiple boxes, remote grandparents — is version two, and the architecture below is arranged so that none of them require a rewrite.
2. The landscape
What you can buy
| Product | Player | Per story | How the card works |
|---|---|---|---|
| Toniebox 2 | $129.99 | $19.99 | NFC figurine; audio downloads from the vendor cloud on first play, then cached |
| Yoto Player (3rd gen) | $109.99 | $9.99–$14.99 | NFC card carrying a link; audio streams or caches from the vendor |
| Yoto Mini | $79.99 | $9.99–$14.99 | as above |
Note what both of them do with the card: the card is a pointer, not a container. Neither commercial product puts audio on the tag either. Over two years a Toniebox with a modest collection runs $610–$850, a Yoto $350–$470 — almost all of it content.
What has already been built by other people
| Project | Brain | Shape | What we take from it |
|---|---|---|---|
| TonUINO | Arduino Nano | MFRC522 reader + DFPlayer Mini MP3 module + buttons | Proof that the cheap, instant-on, no-OS version works. Boots in under a second, sips power. Its limit is that a DFPlayer is a sealed black box: no WiFi ingest, so new content means pulling the SD card. |
| Phoniebox (RPi Jukebox RFID) | Raspberry Pi | USB RFID reader, mpd, web UI, Spotify/streams | Proof that the web-managed version is what parents actually want. Its limit is boot time — well over a minute from cold — and a filesystem a toddler can corrupt by pulling the plug. |
The gap between them is the design space we are in: TonUINO’s instant-on with Phoniebox’s ingest story. That is the whole product thesis, and it is what pushes us toward an ESP32-class part rather than either an AVR or a full Linux box.
3. Four corrections
The obvious version of this project contains four assumptions. Each is wrong, and each one being wrong makes the design simpler, not harder.
3.1 The card holds no audio — and cannot, by three orders of magnitude
The intuition that audio is small and tags are big is exactly inverted. Here is the arithmetic laid side by side:
| Tag | User memory | Speech it could hold at 24 kbps |
|---|---|---|
| MIFARE Ultralight | 48 B | 0.016 s |
| NTAG213 (the 20¢ sticker) | 144 B | 0.048 s |
| NTAG215 (the Amiibo chip) | 504 B | 0.168 s |
| NTAG216 (biggest cheap tag) | 888 B | 0.30 s |
| MIFARE DESFire EV3 8K (expensive) | 8,192 B | 2.7 s |
| One minute of a parent reading | 183,600 B | — |
| A 20-minute picture book | 3,672,000 B | — |
A picture book is 4,100× the biggest cheap tag. There is no clever codec that closes a gap that size; at the bitrate where 20 minutes fits in 888 bytes you have 6 bits per second, which is roughly enough to transmit the title in Morse.
So the card carries a 64-bit identifier, and the box owns the audio. Four things fall out of that, all of them good:
- Many books per card is free. It was on the list as a stretch goal. In a pointer design a card maps to a playlist, and a card holding six books differs from a card holding one by the length of an array. It is in the data model from day one because leaving it out would have taken extra work.
- Re-pointing a card never touches the card. Changing what a card plays is an edit to a JSON file on the box. The card is never rewritten, never wears out, and never has to be found in order to be changed.
- The cheapest tag on the market is the target. Everything is sized to fit NTAG213’s 144 bytes, which is what makes the marginal story cost 20¢.
- Storage stops being a topic. A 32 GB SD card holds about 3,100 hours of speech — on the order of nine thousand picture books. We will never fill it and should never optimise for it.
What actually goes on the tag is two NDEF records:
record 1 URL https://tape.mino.mobi/c/<id> ~35 bytes
— so a card found under the sofa, tapped on any phone, with no app
installed and the box switched off, says what it is.
record 2 external minomobi.com:tape 13–45 bytes
0 'T' 'P' magic
2 version 1
3 flags shuffle | resume | locked
4 card id 8 random bytes
12 label length
13 label UTF-8, up to 32 bytes — a fallback name so an
unknown card can still announce itself
total on tag, worst case: 103 of NTAG213's 144 bytes.
That last line is asserted by a test, not by hope —
tape/lib/tape.selftest.mjs fails the build if a maximal card ever stops fitting an
NTAG213, because the day it stops fitting is the day the project gets more expensive.
The id is rendered as 13 characters of Crockford
base32: no I, L, O or U, and the decoder reads a hand-written O as
0. That is for the case where a parent writes the id on the back of the card with a
biro.
3.2 “RFID” is the wrong radio — this is an NFC project
The two things sold as “RFID reader modules” are not interchangeable here:
| 125 kHz (EM4100 / T5577) | 13.56 MHz NFC (NTAG21x) | |
|---|---|---|
| Tag cost | ~$0.10 | ~$0.20 |
| Reader cost | ~$3 (RDM6300) | ~$2–$6 |
| A phone can read it | no | yes |
| A phone can write it | no | yes (Android) |
| Carries structured data | a fixed serial number only | NDEF records |
| Tap-to-open-a-URL | no | yes |
Saving ten cents a card on 125 kHz costs the entire phone-side pipeline and the card-tells-you-what-it-is affordance. 13.56 MHz, ISO 14443A, NTAG213 or better. Not negotiable; it is the hinge the rest of the product hangs on.
One consequence to design for rather than discover: read range is short and a four-year-old aims badly. These tags work over roughly 1–4 cm and care about orientation. The enclosure needs a recessed nest that puts the card in the right place by geometry, not by the child being careful. That is an enclosure requirement derived from the radio, and it is the kind of thing that is free now and expensive after the case is printed.
3.3 Arduino cannot do Bluetooth audio, and Bluetooth is probably the wrong default anyway
The factual half. No classic Arduino board has a Bluetooth radio at all. Of the parts that do:
- ESP32 (the original WROOM/WROVER) has Bluetooth Classic and can act as an
A2DP source — it can be the thing that sends audio to a Bluetooth speaker. This
works;
pschatzmann/ESP32-A2DPplusarduino-audio-toolsis the known path, and it decodes MP3 or Opus off an SD card into the stream. - ESP32-S3, -C3, -C6, -H2 are BLE-only. They have no Bluetooth Classic and therefore cannot do A2DP at all. This is the trap: the newest, cheapest, most available, best-supported modules are exactly the ones that cannot do the thing. If we commit to Bluetooth output we are committing to a 2016 silicon generation for the life of the project.
- The A2DP library also carries a live maintenance tax — it is known to break on newer Arduino-ESP32 cores, with the working advice being to pin an older core. For a project meant to be built by strangers from a README in three years, a pinned-to-an-old-toolchain dependency is a real cost.
- WiFi and Bluetooth share one radio on the ESP32 and coexistence is genuinely poor. Uploading a recording while a story plays would be a bad time.
The judgement half. Bluetooth was the starting assumption for this build, and the concern is specific rather than aesthetic:
- A Bluetooth speaker is a second device with its own battery and its own power button. The failure mode is a four-year-old putting a card on a silent box.
- A2DP adds roughly 150–400 ms of latency. For playing a story that is invisible. For “I put the card on and nothing happened yet” and for button feedback, it is the difference between a toy that feels alive and one that feels broken.
- Pairing is a state that can be lost, and speakers helpfully re-connect to whichever phone walked into the room.
- It is $25–$40 and one more thing to charge, to lose, and to explain.
An I²S DAC, a 3 W class-D amp and a small full-range driver inside the box come to roughly $8, have no latency, no pairing, and one power switch, and make the box a single object a child can carry.
begin / write(pcm) / setVolume) with two implementations, selected at build time.
Bluetooth then remains genuinely useful for the cases where it is the right answer: playing a
story through the good speaker in the living room, and headphones in the car. If the wired
default turns out to be wrong, the cost of changing our mind is one file. This is flagged for
decision at component selection, not decided here.
3.4 Neither origin can do the whole job — so a file crosses between them
Rule one, which was right. A page served over HTTPS cannot fetch
http://192.168.4.1. It is blocked as mixed content and no header on either side
changes it. The box has no public name and no way to renew a certificate, so it serves plain HTTP.
Uploading can only happen from the box’s own origin.
Rule two, which was missed. getUserMedia() — the microphone
— is gated on a secure context. On a plain-HTTP page
navigator.mediaDevices is not restricted, it is undefined; touching
it throws a TypeError. A LAN IP over HTTP is not a secure context and never will be.
Recording can only happen on https://tape.mino.mobi. Web NFC is gated the
same way, so tag-writing from a phone lands on the same side of the line.
So no single origin has both. That is the whole shape of the pipeline.
| record | write a tag | upload to the box | accept a file | |
|---|---|---|---|---|
https://tape.mino.mobi | yes | Android | no | yes |
http://192.168.4.1 (the box) | no | no | yes | yes |
Look at the last column. A file input works on any origin — it needs no secure context, no permission and no network. So the two halves are joined by something inert: an audio file, which the phone is extremely good at holding on to.
This is asserted rather than remembered: originCapabilities() in
lib/protocol.js is exhaustively tested over every combination, and the test fails if
any of them ever claims both. The impossible combination — secure and on the
box’s origin — throws, because the box serves plain HTTP; passing
boxHasCertificate is the only way to get it, and that models
the upgrade below.
The nice consequence: most parents never open our recorder
If a file has to cross anyway, then the phone’s own voice-memo app is a first class way to make one, and for most people it is the better one. It is already installed. It handles pausing, backgrounding and an incoming phone call in the middle of chapter three — all things we would otherwise implement badly. And the recording lands in the phone’s own storage, which means it inherits the phone’s backups for free. Given that the recordings are the only irreplaceable thing in this project, that is not a small point.
The box accepts what phones make: ESP-ADF decodes MP3, AAC, WAV, OGG, Opus, FLAC and AMR, which
covers iOS Voice Memos (.m4a), Android Recorder (.m4a/.ogg),
and the ripped audiobook MP3s a parent probably already owns. Our studio remains worth using for
what it adds — splitting a book into tracks as you read it, so “go back a bit”
lands on a chapter — but it is an option, not a gate.
The upgrade that would collapse this to one page
There is a known trick, and Plex ships it: publish a wildcard DNS record whose names resolve to
private addresses (192-168-4-1.box.tape.mino.mobi → 192.168.4.1) and get
a real certificate for it. The box’s own page is then HTTPS: secure context, no mixed
content, record and upload together. It is genuinely the right long-term answer.
It is not in version one for three concrete reasons: the certificate’s private key would
ship inside every firmware image and therefore be public; it expires every ninety days, which
turns a toy into something that must be maintained; and on the box’s own access point there
is no internet to resolve the name with, so the box would also have to run a DNS server answering
for it. Each is solvable. None is solvable in the first build. It is recorded in
TLS_UPGRADE so the next person does not have to rediscover it.
3.5 WiFi, not Bluetooth — and the box brings its own network
Bluetooth was one of the two candidates for getting the file across. It loses on two independent counts, either of which would be enough:
| BLE | Bluetooth Classic (SPP) | WiFi | |
|---|---|---|---|
| Real throughput | ~15 kB/s | ~100 kB/s | ~1–3 MB/s |
| A 20-minute book | 4 minutes | 36 s | 2–4 s |
| A browser can speak it | Chrome/Android only | no browser, ever | it is just HTTP |
| On iOS | never — Apple has said so | native app only | yes |
BLE is the only Bluetooth a browser can reach, it is four minutes a book, and Safari has never shipped Web Bluetooth and has stated it will not. SPP is fast enough but no browser speaks it at all, so it means a native app on two platforms. Neither is worth it when the alternative is an ordinary HTTP POST.
The specific WiFi shape is where the design earns something. The box runs SoftAP and station mode at the same time:
- Access point — the box publishes
tape-a4f2athttp://192.168.4.1, with a captive portal so joining it opens the page by itself. This is the mode that needs no router at all: a grandparent’s spare room, a holiday cottage, the back of a car. It is also first-run setup, so the box works out of the box before it knows anything about your house. - Station — give it your home WiFi once and it also joins your LAN, so from then on there is no network switching: you open its address from your normal network.
A literal IP rather than tape.local is deliberate for the AP path: Android does
not resolve .local reliably, and this is not the moment to depend on mDNS. The name is
offered as a convenience on the LAN path, where a failure is recoverable.
3.6 The whole flow, end to end
Once, ever: power the box, join tape-a4f2
on your phone, and the setup page opens itself. Optionally hand it your home WiFi.
- Record. Either the phone’s voice-memo app — nothing to install,
and it is backed up — or the studio on
https://tape.mino.mobi, which splits the book into tracks as you read. - Open the box. Its address on your home WiFi, or join its access point.
- Choose the file, name the book, upload. The box writes
/tape/audio/<title>/and adds it to the manifest. - Put a card on the pad. The box writes the sticker with a fresh id and binds it to the book. This is why the box needs a reader that can write.
- Put the card in the book. That is the filing system.
No app. No account. No cloud. No Bluetooth pairing. The only thing that ever needed the internet was loading our studio, and even that is optional.
The relay stays in the design as an opt-in extra, and it is the one path that touches a server: the studio encrypts a recording in the browser and parks the ciphertext in an inbox the box pulls over HTTPS. It exists for exactly one case — a grandparent in another city recording a story — and the server only ever holds a blob it cannot read.
4. Product architecture
The rule that keeps it simple
Ingest runs only when nothing is playing. WiFi comes up when the box is idle and goes down when a card lands. That single rule removes the WiFi/Bluetooth coexistence problem, removes SD contention between the decoder and the uploader, removes a class of audio glitches, and costs nothing a parent would notice — nobody uploads a story while their child is listening to one.
The data model, in three files
The architecture is written as code, not only as prose, so that it is testable and so the firmware and the studio cannot drift apart. All three are dependency-free ES modules that run in a browser and in node:
| File | Owns |
|---|---|
tape/lib/tag.js | What is on a card. Byte layout, Crockford base32 ids, the NDEF records, and the size check that keeps NTAG213 the target. |
tape/lib/catalog.js | The manifest: titles, cards, playlists, SD paths, validation, orphan sweep, and the size arithmetic quoted on this page. |
tape/lib/protocol.js | The box HTTP API, the SSE events, the network modes, and originCapabilities() — the two browser rules expressed as a function, so “no origin can do both” is tested rather than remembered. |
tape/lib/pinmap.js | Which ESP32-S3 pin does what, and the ones you may not have — GPIO33–37 carry the octal PSRAM and GPIO22–25 do not exist. The schematic is drawn from this, so the picture and the firmware cannot disagree. |
tape/parts.json | The bill of materials, priced two ways, with a verified-link verdict per source. check-links.mjs refreshes them. |
tape/lib/power.js | The power budget: per-component draw in each state, runtime on a given cell, days between charges. Rendered live on the hardware page. |
tape/lib/tape.selftest.mjs | 37 known-answer checks, run by scripts/preflight.mjs and by the deploy workflow before it ships. |
The manifest is the box’s entire state, and it is one file:
/tape/manifest.json
{
"schema": 1,
"titles": {
"wild-things": { "title": "Where the Wild Things Are", "reader": "Dad",
"tracks": [ { "file": "/tape/audio/wild-things/000.opus",
"name": "The whole thing", "seconds": 380 } ] }
},
"cards": {
"4S9NY0QP0TZEC": { "label": "bedtime", "mode": "sequence", "resume": true,
"playlist": [ "wild-things", "owl-babies" ] }
}
}
Track file paths are derived from the title id and never author-supplied — the validator rejects a manifest where they disagree, which closes the obvious path traversal and the obvious way to end up with a card pointing at a file that is not there.
The box API
GET /api/hello version handshake; the studio refuses to write
to firmware it does not understand
GET /api/manifest the whole catalogue
GET /api/events SSE: card | playback | enrolled | ingest | error
POST /api/title upload a new recording
DELETE /api/title/:titleId
PUT /api/card/:cardId bind a card to a playlist
DELETE /api/card/:cardId
POST /api/enroll arm the box to write the next blank tag
POST /api/say speak a line through the box (for setup + testing)
5. The interaction model
Presence, not events
The reader is polled continuously, and the card being there is the play state. Card arrives → play. Card leaves → pause and remember. There is no play button and a four-year-old needs no instruction, because “take it off to stop it” is a thing hands already know.
This is a firmware requirement rather than a free lunch: RF reads are flaky, so “the card left” must survive three consecutive missed reads before it is believed, or a story will stutter every time the child leans on the table.
Cold start is a product feature
Power to “a card works” should be under two seconds. This is the single strongest argument against a Raspberry Pi and the reason TonUINO’s architecture is worth taking seriously: a child does not wait 25 seconds, they conclude it is broken and go and do something else.
Controls
The brief called these a bonus. Proposed set, and one of them is a safety feature:
- Back — jumps back 30 seconds on short press, previous track on hold. This is the button that actually gets used, because children ask for the bit with the monsters again.
- Next — next track; on a single-track title, forward 30 seconds.
- Volume: a rotary potentiometer, not buttons. A pot has a physical end stop, so the maximum volume is set once with a trimmer and a child cannot exceed it. Buttons have no end stop and a four-year-old will find the top of the range on the first day. This is a hearing-safety argument and it is why the extra $1 part is the right call.
- One LED that breathes while playing. No screen — a screen turns this into a device you look at, which is the thing it exists not to be.
Recording, on the phone
- Record it — the phone’s own voice-memo app, or open the studio for track splitting.
- Type the title, hit record, read the book. Pause between chapters if you like — each resume starts a new track, which is how “go back a bit” gets useful boundaries.
- The browser encodes Opus at 24 kbps mono, which is a good voice at 180 kB a minute.
- Upload. The box writes it under
/tape/audio/<title-id>/and adds it to the manifest. - Put a blank card on the pad. The box writes it and binds it. Done — or, on Android, write the card from the phone directly and skip the trip to the box.
6. Site architecture
This surface is not a brochure for the project; it is part of the project. Three of its pages are the actual software.
| Path | What it is | Runs where |
|---|---|---|
/ | The pitch, the diagram, the arithmetic | here |
/design/ | This document — the decision record | here |
/hardware/ | Bill of materials with checked links, schematic, power chain, live power budget | here, rendered from parts.json and lib/pinmap.js |
/firmware/ | Firmware architecture, the state machine, releases | here |
/studio/ | The recorder. Record a book in tracks, encode, hand back a file | here, and only here — the microphone needs HTTPS (§3.4). The box serves a separate, much simpler receiving page |
/tags/, /c/<id> | The card writer. The byte layout, a Web NFC writer, and the page every card’s URL points at | here (Web NFC needs HTTPS) |
/sim/ | The box simulator. The whole interaction, in a tab | here |
/build/ | Assembly — eight stages, each ending at a test you can fail | here |
/first-story/ | The instructions. Fifteen minutes from a box on the table to a child hearing you read | here |
Why the studio is not on the box
It cannot be. The microphone needs a secure context and the box cannot serve one (§3.4). So the surface splits along the capability line rather than by taste: this site records and writes tags; the box receives files and writes cards. The box’s page is a file input, a text field and a progress bar — small enough to live in the firmware image and never drift.
Why there is a worker at all
Everything here is static except one route. /c/<cardId> — the URL every
card carries as its first NDEF record — is rewritten to the card page, so that tapping a
stray card on any phone, with no app installed and the box switched off, tells you what it is.
That affordance is worth 25 lines of worker.
Why the simulator exists
The sandbox this repo is developed in cannot touch hardware, and hardware has not been bought. The simulator is where the interaction gets designed and got wrong: presence-based playback, the debounce, resume behaviour, what a card with six books does when you take it off during book four. Those are the decisions that make the toy feel right, and none of them need a soldering iron to discover. It also stays useful afterwards, as the place a firmware behaviour change can be argued about before it is written.
On disk
tape/
CLAUDE.md instructions for this surface
wrangler.jsonc worker `tape`, custom_domain tape.mino.mobi
worker.js static assets + the /c/<id> rewrite
index.html the pitch
styles.css one stylesheet, no build step
design/ this document
hardware/ firmware/ build/ documentation pages
studio/ tags/ sim/ the working software
lib/
tag.js catalog.js protocol.js the model, shared with the firmware
tape.selftest.mjs 19 checks, run by preflight and by deploy
7. Component selection — answered
All six are resolved. The parts, the candidates they beat, and the live power budget are on the hardware page; the short version:
| # | Question | Answer | The deciding fact |
|---|---|---|---|
| Q1 | Audio out: wired or Bluetooth? | Wired — MAX98357A I²S amp + 4Ω driver | Settled by you: the wireless speaker is not important. That one sentence is what unlocked Q2. |
| Q2 | The brain | ESP32-S3-WROOM-1 N16R8 | With A2DP gone, nothing holds us to 2016 silicon. ESP-ADF on the S3 decodes every format a phone produces, which is what makes the voice-memo path work. |
| Q3 | NFC reader | PN532 | The box has to write tags, not just read them — on its own access point there is no internet to load a phone-side writer with. MFRC522 would mean writing the NDEF layer ourselves. |
| Q4 | Battery or cable? | Battery — protected 18650, MCP73871 power path | ~20 h of stories, ~8 days between charges. The budget also disqualified the IP5306: its boost cuts out below ~45 mA and idle is 13 mA, so the box would switch itself off waiting for a card. |
| Q5 | Codec | Whatever arrives; MP3 64k mono from the studio | ESP-ADF decodes MP3/AAC/WAV/OGG/Opus/FLAC/AMR. Deciding a codec turned out to be unnecessary, which is the best kind of answer. |
| Q6 | Enclosure | still open | But no longer unconstrained — five requirements have accumulated from decisions elsewhere. Listed. |
8. Decision log
| # | Decision | Status | Because |
|---|---|---|---|
| D1 | The card holds a 64-bit id, never audio | settled | 4,100× too small; and pointers give playlists and re-pointing for free (§3.1) |
| D2 | 13.56 MHz NFC, NTAG213 as the floor | settled | 125 kHz cannot be read or written by a phone at all (§3.2) |
| D3 | Two NDEF records: a URL for humans, an external record for the box | settled | a lost card should be able to say what it is; 103 of 144 bytes worst case |
| D4 | Many books per card is in the data model from day one | settled | it is the length of an array; omitting it would be extra work |
| D5 | revised | Corrected. The mixed-content half was right; the microphone half was wrong. getUserMedia needs a secure context, so recording cannot happen on the box (§3.4) | |
| D6 | The box is the tag writer of record; Web NFC is a shortcut | settled, and stronger | Web NFC is Android-Chrome-only and needs HTTPS — but on the box’s access point there is no internet to load an HTTPS page with. The box is the only writer always present (§3.4) |
| D7 | Presence is the play state; no play button | settled | hands already know it; needs a 3-read debounce in firmware (§5) |
| D8 | Ingest is awake only when nothing is playing | settled | removes radio coexistence and SD contention at zero user cost (§4) |
| D9 | Volume is a potentiometer with a physical end stop | settled | a hard maximum a child cannot exceed — hearing safety (§5) |
| D10 | Audio output is wired; the swappable stage stays but has one implementation | settled | Q1 answered: the wireless speaker is not important, which freed the brain choice entirely (§3.3) |
| D13 | WiFi, never Bluetooth, for moving files | settled | BLE is ~4 minutes a book and Safari will never ship Web Bluetooth; SPP is fast but no browser speaks it (§3.5) |
| D14 | The box runs SoftAP and station mode together | settled | AP means it works with no router at all — a grandparent’s house, a car; STA means no network switching at home (§3.5) |
| D15 | The phone’s own voice-memo app is a first-class input | settled | Nothing to install, handles interruptions properly, and the recording inherits the phone’s backups — which matters most for the one irreplaceable asset (§3.4) |
| D16 | The card goes in the book | settled | The pairing between book and recording is maintained by the object, in the place you next look. Cards are consumable at ~14¢; the recordings are the asset |
| D17 | The reader refuses to act on more than one tag in the field | settled | A deck of cards is a stack by nature; two tags is an anticollision event, not a read |
| D18 | The NFC field is duty-cycled, 10 ms in every 125 | settled | A continuous field costs 50–80 mA and would end the portable version on its own (budget) |
| D11 | Relay is opt-in, end-to-end encrypted, off by default | settled | a child’s family recordings should not sit on anyone’s server in the clear |
| D12 | Cold start under two seconds is a requirement, not a preference | settled | it is the difference between a toy and a broken toy — constrains Q2 |
9. Sources
- NXP, NTAG213/215/216 datasheet — user memory figures
- Seritag, which NFC chip and NFCore, capacity by chip — the capacity table
- Chrome for Developers, Web NFC and MDN, Web NFC API — availability and the
NDEFReadercontract - WebNfc.org, browser support — the ~6% figure and the Safari position
- pschatzmann/ESP32-A2DP and Espressif A2DP API — what A2DP source needs and which parts have it
- Phoniebox (RPi-Jukebox-RFID) and TonUINO — prior art
- Yoto vs Toniebox, true cost and Toniebox alternatives — 2026 pricing
- MDN,
getUserMedia()— the secure-context rule that forced the correction in §3.4 - Can I use, Web Bluetooth and BLE throughput to an ESP32 — why Bluetooth loses (§3.5)
- Espressif, ESP32-S3 module current measurements and the MAX98357A datasheet — the power budget’s inputs
- ESP-ADF — the decoder list that makes any phone recording acceptable
- Crockford base32 — the id alphabet
Next: component selection — the parts are chosen and the power budget is live. What is left before anything is bought: the enclosure (Q6), the wiring diagram, and a first board on a bench.