100%
digits codec view
Description

Imported from GitHub: justinlindh/digits · commit e1f9b7d · license AGPL-3.0

Description

Encrypted retro phone network — private VoIP over vintage desk phones

README


Each phone is a gutted vintage desk phone with two processors inside: an RP2040 handling real-time phone hardware (keypad scanning, hook switch, bell ringer, DTMF tones) and a Raspberry Pi Zero 2 W running a Go daemon for VoIP, end-to-end encrypted media (DTLS-SRTP), and call signaling. A Go server handles call routing, device pairing, and household management. There's a free public instance at app.digits.family, or you can run your own.

Table of Contents

Architecture

ComponentRole
RP2040 (firmware, C)Real-time phone I/O: keypad matrix, hook switch, bell driver, DTMF/tone generation, status LED. Communicates with the Pi over UART. V0/V1 use a Pico H module; V2/V3 have the RP2040 onboard.
Pi Zero 2 W (digitsd, Go)VoIP daemon: WebRTC media, Opus codec, DTLS-SRTP, signaling client, call state machine, Wi-Fi setup, OTA updates.
Audio codecMic input and earpiece output. Codec Zero HAT (DA7212) on V0/V1, onboard TLV320AIC3104 on V2/V3.
Signaling server (Go)WebSocket relay for SDP/ICE exchange, PostgreSQL persistence, device pairing, household management, web dashboard.

Calls are peer-to-peer WebRTC sessions between two phones (or three, for party-line calls). The server brokers the signaling handshake but never handles media.

See Architecture deep-dive for the full call path, data model, and NAT traversal.

Hardware

Four hardware iterations, each a different build strategy:

RevConstructionAudioRingerStatus
V0Perfboard, off-the-shelf modulesCodec Zero HAT (DA7212)L298N + step-up transformerDone, documented end-to-end
V1Hand-assembled PCB, Pico H moduleCodec Zero HAT (DA7212)L298N + step-up transformerFabricated, has errata
V2Contract-assembled (JLCPCB), onboard RP2040Onboard TLV320AIC3104Onboard DRV8871Deployed, ~10 units in use
V3Contract-assembled (JLCPCB), onboard RP2040Onboard TLV320AIC3104Onboard DRV8871 + XL6019 boostIn progress, see changes

V1 is a bench-build target: single-sided, mostly through-hole, hand-solderable. External modules handle audio (Codec Zero HAT) and bell ringing (L298N H-bridge plus a step-up transformer).

V2 is a fab-service target: onboard RP2040, audio codec (TLV320AIC3104, 32-pin QFN), and ringer driver (DRV8871). Arrives pre-assembled from JLCPCB. Not practical to hand-solder.

V3 is a major spin of V2: +5 V input (the 12 V buck stage is gone), an on-board XL6019 boost converter making ~37 V to ring the bell in place of the external step-up transformer, a single DPDT cradle switch for hook sense and series mic-kill, an SW2 BOOTSEL button, power indicator LEDs, components flipped to face up, and J8/LED pinouts matched to the stock donor-phone cables. Like V2, it arrives pre-assembled from JLCPCB.

Schematics, PCB layouts, Gerbers, and BOMs are in hardware/pcb/. All designed in KiCad. The firmware abstracts board differences at runtime, so the same binary runs on V1, V2, and V3.

Project Structure

firmware/       RP2040 firmware (C, CMake, Pico SDK)
pi/digitsd/     Pi-side VoIP daemon (Go, cross-compiled to arm64)
pi/image/       Raspberry Pi OS image builder
server/         Signaling server + web dashboard (Go, htmx, Tailwind)
hardware/pcb/   KiCad schematics, PCB layouts, Gerbers, BOMs
charts/digits/  Helm chart for Kubernetes deployment
tools/          Build scripts for firmware, Pi binaries, and OS images
docs/           Architecture notes, build guides, self-hosting
scripts/        Firmware build and flash helpers

Quick Start

Server

cd server
make build     # builds bin/signald
make run       # build + run (defaults to :8443)
make test      # go test ./...

Requires Go 1.26+ and PostgreSQL. The server reads DATABASE_URL and runs migrations on startup.

Firmware

./scripts/build.sh     # auto-detects PICO_SDK_PATH
./scripts/flash.sh     # hold BOOTSEL, plug in USB, then run

Pi Daemon

cd pi/digitsd
make build             # cross-compiles to linux/arm64 via Docker
make build-local       # native build (requires cross-compile libs)
make test

Hosting

The server is a single Go binary (signald) backed by Postgres. For calls across different networks, you also need a TURN relay (coturn). Docker Compose is the straightforward way to run both: clone the repo, fill in an .env file, docker compose up -d. The self-hosting guide covers the full setup including TURN, TLS, SMTP for magic-link auth, phone pairing, backups, and troubleshooting.

There is also a Helm chart if you happen to have a Kubernetes cluster. There is no good reason for a family phone network to run on k8s, but I over-engineered almost everything in this project and it would have felt wrong to stop at the deployment layer. The chart supports CNPG Postgres, Redis Sentinel for multi-replica signaling, OpenTelemetry tracing, Pyroscope profiling, and Prometheus metrics. It is what runs in production. Completely unnecessary, but it works and it is there if you want it.

Web App

The server includes a web dashboard for pairing phones, organizing households, viewing call history, and configuring per-line settings.

Service Codes

Hidden codes entered on the keypad while the handset is off-hook.

CodeActionDetails
*#*0 -- *#*9VolumeSets earpiece volume (0 = quiet, 9 = max). Persists across reboots.
*#8378#Audio testRecords 5 s from mic, plays it back through earpiece. (*#TEST# on keypad.)
*#*#ShutdownGraceful power-off. Safe to unplug after LED goes dark.
*##*RebootImmediate reboot.
*#0*Force re-pairClears device token, reboots into pairing mode.
*#73887#Wi-Fi setupReboots into AP mode for Wi-Fi reconfiguration. (*#SETUP# on keypad.)
*#873283#Update checkChecks for OTA firmware and daemon updates. (*#UPDATE# on keypad.)
*#00000#Factory resetWipes config, Wi-Fi, contacts. Fresh AP + pairing mode.

Call Return (*69 / *89)

CodeActionDetails
*69Call returnAnnounces who last called you. Press 1 to call them back. If the line is busy, the system retries for 30 minutes and rings you with a distinctive pattern when the line is free.
*89Cancel call returnCancels any pending *69 busy-retry. Voice confirmation plays.

These are dialed from dial tone (not prefixed with *#), matching the original 1990s POTS behavior.

Voicemail (*96 / *97 / *98 / *99)

CodeActionDetails
*96Audition greetingPlays your current outgoing greeting through the earpiece, so you can hear what callers hear.
*97Record greetingRecords a custom outgoing greeting after the tone.
*98Listen to messagesPlays your messages oldest first, then any saved (already heard) messages. During playback: 7 deletes, 9 saves, # skips, * replays.
*99Delete greetingRemoves your custom greeting and restores the default.

When a call rings unanswered past the configured timeout, the phone answers it, plays your greeting, and records the caller's message. The handset microphone is muted while recording, so a caller never hears your room. See the voicemail guide for using and configuring it, or Voicemail for the engineering reference: FSM, on-disk storage format, signaling, and configuration.

Confirmation feedback

  • Volume change. 1 beep, then dial tone resumes.
  • Audio test. 2 beeps (start speaking), 1 beep (playback done), dial tone resumes.
  • Shutdown. 3 beeps, then powers off.
  • Reboot. 2 beeps, then reboots.

Party Line (Three-Way Calling)

Classic 90s residential three-way calling. During an active call, press and briefly release the hook switch (a "flash", 100-600 ms on-hook) to get a second dial tone. Dial a third number, talk to them privately, then flash again to merge all three into a single call.

GestureWhat happens
Flash during an active callHeld party drops to silent hold; you hear a second dial tone
Dial a numberRingback; third party's phone rings
Flash again after they answerAll three merged into the conference
Flash before they answerDrops the add attempt; returns to the held party
Hang up (host)Collapses the conference for everyone
Hang up (non-host)Remaining pair keeps talking as a normal 2-party call

Hard-capped at three parties, matching residential TWC on 5ESS / DMS-100 switches. Audio stays end-to-end encrypted on three independent DTLS-SRTP legs; the server never sees media, and mixing happens locally on each phone.

See Party Line for the state machine, media topology, signalling protocol, and failure-mode coverage.

Easter Eggs

Hidden sequences that play audio clips through the earpiece. Each keypress must be within ~1.5 s of the last.

SequenceWhat plays
5-5-4-2Towelie: "That's it! That's the melody to Funky Town!"
0-0-0-0Rick Astley: "Never gonna give you up..."
8-6-7-5-3-0-9Tommy Tutone: "Jenny I got your number... 867-5309!" (intercepts the dial)

Privacy

Calls use WebRTC with DTLS-SRTP. Voice data is encrypted end-to-end between the two devices on the call. The signaling server facilitates connection setup but never has access to the audio stream. Even if the server were compromised, there would be no voice data to extract.

A dedicated hardware kill switch (separate from the hook switch) physically disconnects the microphone circuit when the handset is on the cradle. This is an electrical disconnect, not a software mute.

What the server stores: user accounts (email, name), household membership, phone line assignments, device pairing tokens, session data, call metadata (caller, callee, timestamps, duration), and connection quality telemetry (packet loss, jitter, bandwidth).

What the server does not store: voice audio, call recordings, transcripts, or location data. There is no mechanism in the server to capture call audio.

The server is designed to be self-hosted. If you do not want to use the public instance at app.digits.family, you can run the same code on your own infrastructure. See Self-hosting.

Contributing

This is a personal project, not a company. Contributions to both the software and the hardware are welcome.

On the software side, the server is vanilla Go with htmx and Tailwind, the Pi daemon is Go with WebRTC, and the firmware is C on the Pico SDK. Standard open source workflow: fork, branch, PR.

On the hardware side, I am not an electrical engineer. I learned KiCad, read datasheets, and iterated until the board worked. The result is a functional PCB with minimal errata, which I'm genuinely proud of, but there is a lot of room for someone with real PCB design experience to improve the layout, power integrity, signal routing, and DFM. If you have those skills and this project interests you, I would particularly welcome that kind of help. The schematics and board files are all KiCad and licensed CC BY-SA 4.0.

Documentation

License

Code (firmware, server, tooling): AGPL-3.0

Hardware (schematics, PCB layout, BOM): CC BY-SA 4.0

Comments
Sign in to comment

No comments yet. Be the first to ask about this board.