Why
Our family is spread across countries and I wanted my kid to call them without a grown-up dialing. Kid phones you can buy come with a monthly plan and an app. This one is a dev board, a $8 handset and some arcade buttons.
How a call works
- Button press on a GPIO.
- The ESP32-S3 sends a SIP INVITE (digest auth) to a Twilio SIP Domain in the parent's own Twilio account.
- The SIP Domain hands the call to a small Twilio Function that holds the allowlist. It refuses any number that is not on it, then dials the real number from the parent's Twilio caller ID.
- Audio is G.711 mu-law at 8 kHz, RTP over UDP.
The handset is the fun part
It is sold as a "retro handset for mobile phones": a USB-C plug with a USB Audio Class chip inside (mine enumerates as "AB17X USB Audio", 001f:0b23, 48 kHz stereo). The ESP32-S3 runs as USB host and talks UAC to it directly, so there is no codec, amp or analog audio on the board at all.
Things that took a while:
- Power out of the USB port. The dual USB-C ESP32-S3 dev boards have a USB-OTG solder pad that lets the native USB port source 5 V. Bridge it or the handset gets no power. (The IN-OUT pad next to it does nothing for this.) After that, the COM port is for the computer or charger and the USB port is for the handset only; a computer on the USB port would get 5 V pushed into it.
- USB host buffers. The earpiece endpoint has a 384-byte max packet size. The default ESP-IDF USB host config could not handle it; it needed
CONTROL_TRANSFER_MAX_SIZE1024 and a custom FIFO split (non-periodic TX 16, periodic TX 96, RX 88 of the 200 lines). - NAT and early media. With Twilio's
answerOnBridgeon, the call gets early media and then a 200 OK, and behind home NAT all audio vanished. Turning it off fixed it. - Twilio error 13225 ("number is blacklisted"). If your Twilio account was created outside the US/Canada and only has an individual profile, calls to +1 numbers get blocked if they aren't on the approved list.
Setup for non-technical parents
No toolchain:
- Flash from Chrome or Edge with ESP Web Tools, then type the WiFi name and password into the installer (Improv WiFi over serial).
- Open
kidphone.local, paste the Twilio Account SID and Auth Token, add people. - The phone provisions the Twilio account by itself: a SIP credential with a per-phone login, the SIP domain, the Function holding the allowlist, and a restricted API key. Then it forgets the Auth Token.
- The restricted key can only update the allowlist variables and redeploy the Function. It gets 401 on calls, the account and everything else. So changing grandma's number later is just Save, no token, and a leaked key cannot place calls.
- Every step is rerun-safe, so a setup that dies halfway is fixed by running it again.
Lights and call hours
Each button's lamp means "you can call now". Each person can have call windows in their own time zone (the firmware carries its own POSIX TZ rule evaluator, so grandma's 6 pm stays 6 pm across her DST change). Outside the window the lamp is off and a press gets a spoken "They can't talk right now." Until NTP syncs, everyone counts as available; a clock problem never blocks a call. There is also a household bedtime window.
Talks instead of beeps
Errors are short spoken prompts generated offline with Piper TTS and stored in flash ("The phone isn't connected. Ask a grown-up for help."). Other kid-proofing: a 30-minute call cap, a 3 s cooldown so button mashing cannot fire off a burst of calls, a silent earpiece while idle (a handset left off the hook does not beep all day), and a factory reset on the board's BOOT button, inside the case.
Privacy
The phone talks only to the parent's Twilio account and an NTP server. It cannot ring (no inbound calls by design), it does not record anything, and it only dials numbers on the list. It is not an emergency phone.
Cost to run
About $2 to $3 a month: $1.15 for the US number plus per-minute charges.
Newer features (0.5 to 0.7)
Assignable pins....
Read more »
Allan Binder