Imported from GitHub: kmatzen/millennium · commit 4867b4f · license MIT
Description
I took an old pay phone from the 2000's and replaced the brains with a raspberry pi and some arduinos.
README
Millennium Project
The Millennium Project breathes new life into the Nortel Millennium public telephone by replacing much of its original electronics with modern hardware and software. The project integrates a Raspberry Pi Zero 2 W, two Arduinos, and a VoIP software stack while retaining all the original hardware, including the display, keypad, coin acceptor, magstripe reader, handset, and ringer speaker.
Table of Contents
- Overview
- Architecture
- Features
- Directory Structure
- Setup
- Configuration
- Development
- Resources
- License
Overview
This project reimagines the functionality of the Nortel Millennium telephone by combining modern hardware and software while preserving its iconic hardware features:
- Raspberry Pi Zero 2 W: Manages the system, runs the VoIP stack, and interfaces with the original hardware.
- Arduinos: Control the keypad, display, and other peripherals.
- Custom PCB: Consolidates the connections between components.
- Host Software: Includes a daemon, plugin system, web dashboard, and ALSA audio configuration.
- Preserved Hardware: The original display, keypad, coin acceptor, magstripe reader, handset, and ringer speaker are retained and functional.
Architecture
┌────────────────────────── Raspberry Pi Zero 2 W ───────────────────────────┐
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────────────────┐ │
│ │ Daemon │────▶│ PJSIP (VoIP) │ │ Web Dashboard :80 │ │
│ │ │ └──────────────┘ │ │ │
│ │ Plugins: │ │ - phone state │ │
│ │ - Phone │ ┌──────────────┐ │ - plugin switching │ │
│ │ - Fortune │ │ ALSA Audio │ │ - OTA updates │ │
│ │ - Jukebox │ └──────────────┘ │ - health / metrics │ │
│ │ - +4 games │ └──────────────────────────┘ │
│ └──────┬───────┘ │
│ │ USB serial (9600 baud) │
└─────────┬──────────────────────────────────────────────────────────────────┘
│
┌─────────┴───────────────── Custom PCB (phonev6) ───────────────────────────┐
│ │
│ ┌──────────────────────┐ I2C ┌────────────────────────┐ │
│ │ Display Arduino │◀─────────│ Keypad Arduino │ │
│ │ (millennium_beta) │ │ (millennium_alpha) │ │
│ │ │ │ │ │
│ │ - VFD display │ │ - 4x7 keypad │ │
│ │ (parallel 8-bit) │ │ - magstripe reader │ │
│ │ - coin validator │ │ - hook switch │ │
│ │ (SoftSerial) │ └────────────────────────┘ │
│ └──────────────────────┘ │
└────────────────────────────────────────────────────────────────────────────┘
Data flow: Keypad/hook/card events originate on the Keypad Arduino, are sent via I2C to the Display Arduino, which forwards them to the Raspberry Pi over USB serial. The daemon processes events through an event processor, dispatches them to the active plugin, and sends display updates back down the same path. VoIP calls are handled by PJSIP (PJSUA C API) with audio routed through ALSA (dmix + route plugins for mono channel splitting).
Features
- Pay phone operation: Insert coins, dial numbers, make VoIP calls — authentic payphone experience
- Plugin system: Swap modes live — Classic Phone, Fortune Teller, Jukebox, plus games (Number Guess, Simon, Dial-A-Joke, Trivia). Write your own against the plugin SDK; new plugins appear in the dashboard automatically
- Emergency calls: Dial 911, 311, or 0 without coins (configurable free numbers)
- Magstripe card support: Swipe a registered card for free calling or admin access
- Web dashboard: Real-time phone state, plugin switching, health monitoring via WebSocket
- OTA updates: Check for and apply updates from the web dashboard (git pull, build, restart)
- Audio tones: DTMF, dial tone, busy tone, coin tone, ringback via ALSA
- Idle timeout: Automatically resets phone state after configurable inactivity period
- State persistence: Saves and restores coin balance and plugin state across restarts
- Health monitoring: Tracks serial connection, SIP registration, and daemon activity
- Metrics collection: Counters for calls, coins, keypresses, card swipes
- Version tracking: Build version and git hash displayed on dashboard
- Scenario testing: Simulator-based integration tests with simulated time
Directory Structure
Arduino/: Arduino sketches for the keypad and display microcontrollers, plus a Makefile for building and flashing.case/: 3D model files (.blendand.stl) for a custom enclosure. See case/README.md for dimensions and print settings.pcb/: KiCad schematic, PCB layout, BOM, and Gerber files for the custom PCB (phonev6).HARDWARE.md: Physical assembly reference — USB topology, cable routing, power budget, and component list.host/: Raspberry Pi software:daemon.c— Main daemon loop and event routingplugins/— Plugin implementations (classic_phone, fortune_teller, jukebox, number_guess, simon, dial_a_joke, trivia)plugin_sdk.c— Plugin authoring facade (see PLUGIN_AUTHORING.md)web_server.c— HTTP server with dashboard and REST APIaudio_tones.c— Tone generation (DTMF, dial tone, etc.)updater.c— OTA update checker and appliersimulator.c— Test runner with scenario file support and simulated timetests/— Unit tests and scenario test filessystemd/— Service file for the daemondaemon.conf.example— Example configuration fileasoundrc.example— ALSA audio configuration for mono channel splitting
Setup
1. Arduino Firmware
Navigate to the Arduino/ directory and follow the instructions in its README to build and flash firmware to the microcontrollers.
2. PCB
The PCB files are in the pcb/ directory. Upload the Gerber files to a PCB fabricator like JLCPCB for manufacturing. See the pcb README for details.
3. Host Software
Follow the setup guide for full step-by-step instructions, or use the quick install:
cd host
make daemon # Build the daemon
sudo make install # Install to /usr/local/bin and set up systemd
See the host README for dependency installation (PJSIP, ALSA) and audio configuration.
4. Configuration
Copy and edit the configuration file:
sudo cp host/daemon.conf.example /etc/millennium/daemon.conf
sudo nano /etc/millennium/daemon.conf
See Configuration below for all available options.
Configuration
The daemon reads configuration from /etc/millennium/daemon.conf. See host/daemon.conf.example for a complete annotated example. Key settings:
| Key | Default | Description |
|---|---|---|
hardware.display_device | /dev/serial/by-id/usb-Arduino_LLC_Millennium_Beta-if00 | Serial device for display Arduino |
hardware.baud_rate | 9600 | Serial baud rate |
call.cost_cents | 50 | Cost per call in cents |
call.timeout_seconds | 300 | Maximum call duration |
call.free_numbers | 911,311,0 | Comma-separated numbers that bypass coin requirement |
call.idle_timeout_seconds | 60 | Seconds of inactivity before phone resets |
card.enabled | true | Enable magstripe card support |
card.free_cards | (empty) | Comma-separated card numbers for free calling |
card.admin_cards | (empty) | Comma-separated card numbers for admin access |
web_server.enabled | true | Enable the web dashboard |
web_server.port | 80 | Web dashboard port |
system.source_dir | /home/matzen/millennium | Source directory for OTA updates |
Development
Building and testing
cd host
make test # Build simulator + unit tests, run both
make daemon # Build the full daemon (requires PJSIP/libpjproject on the Pi)
make clean # Remove all build artifacts
Running tests locally
Tests run on any platform (macOS, Linux) without PJSIP or ALSA — the simulator stubs out hardware dependencies:
cd host
make test
This runs:
- Unit tests: Config, daemon state, plugins, emergency numbers, card config, updater
- Scenario tests: Full integration tests (basic call, timeout, emergency, card swipe, hook lifecycle, state persistence, display scrolling, idle timeout)
Adding a plugin
See host/PLUGIN_AUTHORING.md for the full guide. In short:
- Create
host/plugins/your_plugin.c(write it against plugin_sdk.h) - Implement handler functions for the events you care about (coin, keypad, hook, call state, card, tick)
- Register with
plugins_register()in aregister_your_plugin()function - Call the registration function from
plugins_init()inplugins.c - Add the
.ofile to the Makefile build targets (daemon,SIM_OBJS,UNIT_TEST_OBJS) - Run
make test— your plugin then appears in the dashboard automatically
Coding style
- C89 across the whole codebase, compiled with
-std=gnu89(the C89 language plus the GNU extensions the ALSA/PJSIP headers require, e.g.inline; strict-std=c89can't parse those headers) -Wall -Wextra -Wdeclaration-after-statement -Werroron every target — warnings are errors, and-Wdeclaration-after-statementkeeps the source free of C99-style mixed declarations- No unnecessary comments — code should be self-documenting
Resources
This project draws on several resources for understanding and interfacing with the Nortel Millennium telephone's hardware:
-
General Millennium Payphone Documentation The Millennium Payphone Wiki provides extensive documentation on the phone's hardware and protocols.
- Coin Validator Protocol: Used the documented higher-level protocol to interface with the coin validator.
- EEPROM Data: Referenced for configuring the coin validator to accept Canadian currency.
-
Display Documentation A detailed discussion on the Noritake CU20026SCPB-T23A VFD display can be found in this HardForum thread. This was essential for understanding and controlling the display.
-
Device Pinouts The New Fire Millennium Payphone Page includes pinout diagrams for various components, which were critical when developing the Arduino sketches.
-
Millennium Phone Source The Millennium phone used in this project was sourced from Ballard Reuse in Seattle, a store specializing in salvaged and reusable materials.
License
This project is licensed under the MIT License. See LICENSE for details.
No comments yet. Be the first to ask about this board.