Add production WSGI server and CLAUDE.md documentation
- Replace Flask development server with Waitress production WSGI server - Add waitress>=2.1.0 dependency to pyproject.toml and Makefile - Configure 4-thread server for better performance and stability - Create comprehensive CLAUDE.md guide for future development - Document architecture, deployment, testing, and common patterns - Update README.md with production-ready feature and dependencies Eliminates Flask development server warning and provides production-grade web serving suitable for Raspberry Pi deployment. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
322
CLAUDE.md
Normal file
322
CLAUDE.md
Normal file
@@ -0,0 +1,322 @@
|
|||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
|
||||||
|
Wedding Phone is a Raspberry Pi-based rotary phone system for weddings and events. Guests pick up a vintage rotary phone handset to hear a custom greeting, then record voice messages. The system features a Flask web interface for managing greetings and recordings, GPIO hookswitch detection, optional USB backup with CRC32 verification, and HiFiBerry DAC+ADC audio support.
|
||||||
|
|
||||||
|
## Development Commands
|
||||||
|
|
||||||
|
### Running the System
|
||||||
|
```bash
|
||||||
|
# Start the wedding phone system (preferred method)
|
||||||
|
make start
|
||||||
|
|
||||||
|
# Or using UV directly
|
||||||
|
uv run --no-project python rotary_phone_web.py
|
||||||
|
|
||||||
|
# Or using Python directly
|
||||||
|
python3 rotary_phone_web.py
|
||||||
|
```
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
```bash
|
||||||
|
# Run complete audio test (speaker + mic + dial tone)
|
||||||
|
make test
|
||||||
|
|
||||||
|
# Or using UV
|
||||||
|
uv run --no-project python test_complete.py
|
||||||
|
```
|
||||||
|
|
||||||
|
### Dependencies
|
||||||
|
```bash
|
||||||
|
# Install/sync dependencies using UV (preferred)
|
||||||
|
make sync
|
||||||
|
|
||||||
|
# Or manually with UV
|
||||||
|
uv pip install flask numpy pyaudio RPi.GPIO
|
||||||
|
|
||||||
|
# System dependencies (Raspberry Pi)
|
||||||
|
sudo apt-get install -y python3-pyaudio portaudio19-dev
|
||||||
|
```
|
||||||
|
|
||||||
|
### Service Management
|
||||||
|
```bash
|
||||||
|
# Install as systemd service
|
||||||
|
./install_service.sh
|
||||||
|
|
||||||
|
# Service control
|
||||||
|
sudo systemctl start wedding-phone
|
||||||
|
sudo systemctl stop wedding-phone
|
||||||
|
sudo systemctl restart wedding-phone
|
||||||
|
sudo systemctl status wedding-phone
|
||||||
|
|
||||||
|
# View logs
|
||||||
|
sudo journalctl -u wedding-phone -f
|
||||||
|
```
|
||||||
|
|
||||||
|
### Cleanup
|
||||||
|
```bash
|
||||||
|
# Clean temporary files and generated templates
|
||||||
|
make clean
|
||||||
|
```
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
### Single-File Application Design
|
||||||
|
The entire Flask application lives in `rotary_phone_web.py` (~2000 lines). This is intentional - the system is designed for deployment on Raspberry Pi devices where simplicity matters more than strict separation of concerns.
|
||||||
|
|
||||||
|
**Key architectural components:**
|
||||||
|
|
||||||
|
1. **Configuration System** (lines 22-82)
|
||||||
|
- `config.json`: System configuration (GPIO pins, audio device, paths, backup settings)
|
||||||
|
- `user_config.json`: Runtime user settings (active greeting, volume, button sound)
|
||||||
|
- Configuration loaded at startup and used throughout application
|
||||||
|
- User settings persisted on changes (volume adjustments, greeting selection)
|
||||||
|
|
||||||
|
2. **Backup System with CRC32 Verification** (lines 84-218)
|
||||||
|
- `calculate_crc32()`: Compute CRC32 checksum for file verification
|
||||||
|
- `backup_file_to_usb()`: Copy files to multiple USB drives with verification
|
||||||
|
- `get_usb_backup_status()`: Check USB drive mount status and writability
|
||||||
|
- Automatic backup on recording/upload if `backup_on_write` enabled
|
||||||
|
- Corrupted backups automatically deleted
|
||||||
|
|
||||||
|
3. **RotaryPhone Class** (lines 220-550+)
|
||||||
|
- Central state machine managing phone lifecycle
|
||||||
|
- GPIO event-driven architecture with immediate hook detection
|
||||||
|
- Three states: `on_hook`, `off_hook`, `recording`
|
||||||
|
- Audio playback with volume control (numpy-based amplitude scaling)
|
||||||
|
- Recording with minimum duration requirements (1 second)
|
||||||
|
- Extra button support for playing sounds during recording
|
||||||
|
|
||||||
|
4. **Flask Web Application** (lines 550+)
|
||||||
|
- REST API for status, recordings, greetings, volume control
|
||||||
|
- Template auto-generation (creates `templates/index.html` on first run)
|
||||||
|
- File upload handling with werkzeug secure_filename
|
||||||
|
- Audio streaming endpoints for browser playback
|
||||||
|
|
||||||
|
### Phone State Machine Flow
|
||||||
|
```
|
||||||
|
1. ON_HOOK (waiting)
|
||||||
|
→ GPIO detects hookswitch pressed (handset lifted)
|
||||||
|
|
||||||
|
2. OFF_HOOK (playing greeting)
|
||||||
|
→ Optional greeting delay (0-10 seconds)
|
||||||
|
→ Play active greeting with volume control
|
||||||
|
→ GPIO continuously checked for hang-up
|
||||||
|
|
||||||
|
3. RECORDING (recording audio)
|
||||||
|
→ Record audio from microphone
|
||||||
|
→ Extra button can trigger sounds (non-blocking)
|
||||||
|
→ Stop on hang-up or max duration (300s)
|
||||||
|
→ Save if ≥1 second, backup to USB if enabled
|
||||||
|
→ Return to ON_HOOK
|
||||||
|
```
|
||||||
|
|
||||||
|
### Audio Architecture
|
||||||
|
- **PyAudio Instance**: Single shared instance in `RotaryPhone.audio`
|
||||||
|
- **Streams**: Created per-operation (playback/recording) with configured device index
|
||||||
|
- **Volume Control**: Applied in real-time using numpy array manipulation (int16 scaling)
|
||||||
|
- **Extra Button Sounds**: Separate PyAudio instance to prevent blocking main recording
|
||||||
|
- **Hook Detection**: Direct GPIO reads during playback/recording loops for immediate response
|
||||||
|
|
||||||
|
### HTML Template Generation
|
||||||
|
The Flask app generates `templates/index.html` programmatically on first run. The template is defined as a string in the Python code and written to disk. To update the UI:
|
||||||
|
1. Delete `templates/` directory
|
||||||
|
2. Restart the application
|
||||||
|
3. Updated template will be regenerated
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
### config.json Structure
|
||||||
|
All system configuration is in `config.json` (copy from `config.example.json`):
|
||||||
|
- **gpio**: Pin assignments and pressed states (LOW/HIGH) for hookswitch and extra button
|
||||||
|
- **audio**: Device index, sample rate, chunk size, max recording duration
|
||||||
|
- **paths**: Base directory and subdirectories for recordings/sounds
|
||||||
|
- **backup**: USB paths, CRC verification, auto-backup settings
|
||||||
|
- **web**: Port and upload size limits
|
||||||
|
- **system**: Default values for volume, greeting, button sound, greeting delay
|
||||||
|
|
||||||
|
### Finding Audio Device Index
|
||||||
|
```bash
|
||||||
|
python3 -c "import pyaudio; p=pyaudio.PyAudio(); [print(f'{i}: {p.get_device_info_by_index(i)[\"name\"]}') for i in range(p.get_device_count())]"
|
||||||
|
```
|
||||||
|
Update `config.json` → `audio.device_index` with the correct index.
|
||||||
|
|
||||||
|
### GPIO Configuration
|
||||||
|
- **hook_pin**: BCM GPIO pin number for hookswitch
|
||||||
|
- **hook_pressed_state**: "LOW" if switch pulls to ground when pressed, "HIGH" if pulls to 3.3V
|
||||||
|
- **extra_button_enabled**: true/false to enable optional button feature
|
||||||
|
- Pin mode: BCM (not BOARD)
|
||||||
|
- Pull-up resistors enabled on inputs
|
||||||
|
|
||||||
|
## Common Development Tasks
|
||||||
|
|
||||||
|
### Adding New API Endpoints
|
||||||
|
Add Flask routes in `rotary_phone_web.py`. Follow existing patterns:
|
||||||
|
- Status endpoints: Return JSON with `jsonify()`
|
||||||
|
- File operations: Use `secure_filename()` for uploads
|
||||||
|
- Audio streaming: Use `send_file()` with mimetype
|
||||||
|
- Error handling: Return appropriate HTTP status codes
|
||||||
|
|
||||||
|
### Modifying Phone Behavior
|
||||||
|
Edit the `RotaryPhone` class methods:
|
||||||
|
- `play_sound_file()`: Audio playback logic with volume/hook checking
|
||||||
|
- `record_audio()`: Recording loop with GPIO monitoring
|
||||||
|
- `phone_monitor_thread()`: Main state machine loop
|
||||||
|
|
||||||
|
### Adding Configuration Options
|
||||||
|
1. Add to `config.example.json` with documentation
|
||||||
|
2. Load in `load_system_config()` or `RotaryPhone.load_config()`
|
||||||
|
3. Access via `SYS_CONFIG` (system) or `self.config` (runtime user settings)
|
||||||
|
4. Update README.md configuration section
|
||||||
|
|
||||||
|
### USB Backup Modifications
|
||||||
|
- `backup_file_to_usb()`: Handles individual file backup
|
||||||
|
- CRC verification logic prevents silent corruption
|
||||||
|
- Backup called after recording saves and greeting uploads
|
||||||
|
- Test with `/api/backup/test` endpoint
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
### Audio Testing
|
||||||
|
The `test_complete.py` script validates:
|
||||||
|
1. Speaker output (440Hz test tone)
|
||||||
|
2. Dial tone generation (350Hz + 440Hz)
|
||||||
|
3. Microphone recording (5-second test)
|
||||||
|
|
||||||
|
Run before deployment to verify HiFiBerry configuration.
|
||||||
|
|
||||||
|
### Manual Testing Checklist
|
||||||
|
1. Pick up phone → greeting plays
|
||||||
|
2. Hang up during greeting → playback stops immediately
|
||||||
|
3. Wait for greeting → recording starts
|
||||||
|
4. Speak → recording captures audio
|
||||||
|
5. Hang up → recording saves and backs up
|
||||||
|
6. Web interface → upload/play/delete functions work
|
||||||
|
7. Extra button (if enabled) → sound plays during recording only
|
||||||
|
|
||||||
|
## Important Patterns
|
||||||
|
|
||||||
|
### Immediate GPIO Response
|
||||||
|
The system uses direct `GPIO.input()` calls inside playback and recording loops rather than callbacks. This ensures immediate response when the handset is hung up:
|
||||||
|
```python
|
||||||
|
while data:
|
||||||
|
if GPIO.input(HOOK_PIN) != HOOK_PRESSED:
|
||||||
|
print("Handset hung up, stopping playback immediately")
|
||||||
|
break
|
||||||
|
# Continue processing
|
||||||
|
```
|
||||||
|
|
||||||
|
### Volume Control Implementation
|
||||||
|
Volume is applied to PCM audio data using numpy:
|
||||||
|
```python
|
||||||
|
volume = self.get_volume() / 100.0 # 0.0 to 1.0
|
||||||
|
audio_data = np.frombuffer(data, dtype=np.int16)
|
||||||
|
audio_data = (audio_data * volume).astype(np.int16)
|
||||||
|
data = audio_data.tobytes()
|
||||||
|
```
|
||||||
|
|
||||||
|
### File Integrity with CRC32
|
||||||
|
All file writes (recordings, uploads) can be verified with CRC32:
|
||||||
|
```python
|
||||||
|
source_crc = calculate_crc32(source_file)
|
||||||
|
# ... copy file ...
|
||||||
|
dest_crc = calculate_crc32(dest_file)
|
||||||
|
if dest_crc != source_crc:
|
||||||
|
# Corrupted, delete backup
|
||||||
|
```
|
||||||
|
|
||||||
|
## Deployment Notes
|
||||||
|
|
||||||
|
### Raspberry Pi Setup
|
||||||
|
1. Install Raspberry Pi OS (Bullseye or newer)
|
||||||
|
2. Configure HiFiBerry DAC+ADC (`./configure_hifiberry.sh`)
|
||||||
|
3. Wire GPIO hookswitch and optional button
|
||||||
|
4. Create `config.json` from example
|
||||||
|
5. Test audio with `make test`
|
||||||
|
6. Install as service with `./install_service.sh`
|
||||||
|
|
||||||
|
### USB Backup Setup
|
||||||
|
USB drives must be mounted with user write permissions:
|
||||||
|
```bash
|
||||||
|
# Automated setup (recommended)
|
||||||
|
sudo ./setup_usb.sh
|
||||||
|
|
||||||
|
# Or manual mount
|
||||||
|
sudo mount -o uid=$(id -u),gid=$(id -g) /dev/sda1 /media/usb0
|
||||||
|
```
|
||||||
|
|
||||||
|
### HiFiBerry Configuration
|
||||||
|
The system expects HiFiBerry DAC+ADC Pro or compatible. Configuration includes:
|
||||||
|
- Disable onboard audio in `/boot/config.txt`
|
||||||
|
- Enable HiFiBerry overlay
|
||||||
|
- Set correct audio device index in `config.json`
|
||||||
|
See `AUDIO_FIX.md` for detailed troubleshooting.
|
||||||
|
|
||||||
|
### Production Web Server
|
||||||
|
The application uses **waitress**, a production-ready WSGI server for Python web apps. This eliminates the Flask development server warning and provides:
|
||||||
|
- Thread pooling (4 worker threads by default)
|
||||||
|
- Better performance and stability
|
||||||
|
- Production-safe error handling
|
||||||
|
- No "development server" warnings
|
||||||
|
|
||||||
|
The server configuration is in `rotary_phone_web.py` line ~2000:
|
||||||
|
```python
|
||||||
|
serve(app, host='0.0.0.0', port=WEB_PORT, threads=4)
|
||||||
|
```
|
||||||
|
|
||||||
|
To adjust thread count or other waitress settings, modify the `serve()` call parameters.
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
wedding-phone/
|
||||||
|
├── rotary_phone_web.py # Main application (Flask + GPIO + Audio)
|
||||||
|
├── test_complete.py # Audio testing script
|
||||||
|
├── config.json # Configuration (copy from config.example.json)
|
||||||
|
├── config.example.json # Configuration template
|
||||||
|
├── Makefile # Build commands
|
||||||
|
├── pyproject.toml # Python package metadata
|
||||||
|
├── wedding-phone.service # Systemd service template
|
||||||
|
├── install_service.sh # Service installer
|
||||||
|
├── setup_usb.sh # USB setup script
|
||||||
|
├── configure_hifiberry.sh # HiFiBerry configuration
|
||||||
|
├── README.md # User documentation
|
||||||
|
├── CHANGELOG.md # Version history
|
||||||
|
├── AUDIO_FIX.md # Audio troubleshooting
|
||||||
|
└── templates/ # Auto-generated on first run
|
||||||
|
└── index.html # Web interface (auto-generated)
|
||||||
|
```
|
||||||
|
|
||||||
|
Runtime data (auto-created):
|
||||||
|
```
|
||||||
|
rotary_phone_data/ # Configurable via config.json
|
||||||
|
├── recordings/ # Guest voice recordings
|
||||||
|
├── sounds/ # Greeting WAV files
|
||||||
|
└── user_config.json # Runtime settings (volume, active greeting)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
**Core:**
|
||||||
|
- flask >= 2.3.0 (web framework)
|
||||||
|
- numpy >= 1.21.0 (audio processing)
|
||||||
|
- pyaudio >= 0.2.13 (audio I/O)
|
||||||
|
- RPi.GPIO >= 0.7.1 (GPIO control)
|
||||||
|
- waitress >= 2.1.0 (production WSGI server)
|
||||||
|
|
||||||
|
**System:**
|
||||||
|
- portaudio19-dev (PyAudio backend)
|
||||||
|
- Python 3.8+ (required for Flask 2.3+)
|
||||||
|
|
||||||
|
**Development:**
|
||||||
|
- UV package manager (recommended, faster than pip)
|
||||||
|
- Make (command shortcuts)
|
||||||
|
|
||||||
|
## Version Information
|
||||||
|
|
||||||
|
Current version: 1.2.0
|
||||||
|
|
||||||
|
See CHANGELOG.md for complete version history and upgrade notes.
|
||||||
2
Makefile
2
Makefile
@@ -13,7 +13,7 @@ test:
|
|||||||
|
|
||||||
sync:
|
sync:
|
||||||
@echo "Installing/syncing dependencies..."
|
@echo "Installing/syncing dependencies..."
|
||||||
uv pip install flask numpy pyaudio RPi.GPIO
|
uv pip install flask numpy pyaudio RPi.GPIO waitress
|
||||||
|
|
||||||
install: sync
|
install: sync
|
||||||
@echo "Dependencies installed!"
|
@echo "Dependencies installed!"
|
||||||
|
|||||||
@@ -73,6 +73,7 @@ Web interface available at: `http://<raspberry-pi-ip>:8080`
|
|||||||
- **USB Backup**: Automatic backup to multiple USB drives with CRC32 verification
|
- **USB Backup**: Automatic backup to multiple USB drives with CRC32 verification
|
||||||
- **Data Integrity**: Every file write is verified with CRC checksums
|
- **Data Integrity**: Every file write is verified with CRC checksums
|
||||||
- **HiFiBerry Support**: Optimized for HiFiBerry DAC+ADC Pro audio quality
|
- **HiFiBerry Support**: Optimized for HiFiBerry DAC+ADC Pro audio quality
|
||||||
|
- **Production-Ready**: Uses Waitress WSGI server for stable, production-grade web serving
|
||||||
- **Real-time Status**: Monitor phone status (on-hook/off-hook/recording)
|
- **Real-time Status**: Monitor phone status (on-hook/off-hook/recording)
|
||||||
- **Auto-refresh**: Status updates every 5 seconds
|
- **Auto-refresh**: Status updates every 5 seconds
|
||||||
|
|
||||||
@@ -95,6 +96,7 @@ Web interface available at: `http://<raspberry-pi-ip>:8080`
|
|||||||
- numpy>=1.21.0
|
- numpy>=1.21.0
|
||||||
- pyaudio>=0.2.13
|
- pyaudio>=0.2.13
|
||||||
- RPi.GPIO>=0.7.1
|
- RPi.GPIO>=0.7.1
|
||||||
|
- waitress>=2.1.0 (production WSGI server)
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
@@ -127,7 +129,7 @@ sudo apt-get install -y python3-pyaudio portaudio19-dev
|
|||||||
# Install Python dependencies with UV
|
# Install Python dependencies with UV
|
||||||
make sync
|
make sync
|
||||||
# Or manually:
|
# Or manually:
|
||||||
# uv pip install flask numpy pyaudio RPi.GPIO
|
# uv pip install flask numpy pyaudio RPi.GPIO waitress
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Option B: Using pip
|
#### Option B: Using pip
|
||||||
@@ -135,7 +137,7 @@ make sync
|
|||||||
```bash
|
```bash
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
sudo apt-get install -y python3-pip python3-pyaudio portaudio19-dev
|
sudo apt-get install -y python3-pip python3-pyaudio portaudio19-dev
|
||||||
pip3 install flask numpy RPi.GPIO
|
pip3 install flask numpy RPi.GPIO waitress
|
||||||
```
|
```
|
||||||
|
|
||||||
### 4. Configure HiFiBerry
|
### 4. Configure HiFiBerry
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ dependencies = [
|
|||||||
"numpy>=1.21.0",
|
"numpy>=1.21.0",
|
||||||
"pyaudio>=0.2.13",
|
"pyaudio>=0.2.13",
|
||||||
"RPi.GPIO>=0.7.1",
|
"RPi.GPIO>=0.7.1",
|
||||||
|
"waitress>=2.1.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.scripts]
|
[project.scripts]
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import numpy as np
|
|||||||
import threading
|
import threading
|
||||||
from flask import Flask, render_template, request, send_file, jsonify, redirect, url_for
|
from flask import Flask, render_template, request, send_file, jsonify, redirect, url_for
|
||||||
from werkzeug.utils import secure_filename
|
from werkzeug.utils import secure_filename
|
||||||
|
from waitress import serve
|
||||||
import json
|
import json
|
||||||
import sys
|
import sys
|
||||||
import zlib
|
import zlib
|
||||||
@@ -1989,13 +1990,15 @@ def main():
|
|||||||
# Get and display local IP
|
# Get and display local IP
|
||||||
local_ip = get_local_ip()
|
local_ip = get_local_ip()
|
||||||
print("\n" + "="*60)
|
print("\n" + "="*60)
|
||||||
|
print(f"🚀 Wedding Phone System Starting...")
|
||||||
print(f"Web Interface Available At:")
|
print(f"Web Interface Available At:")
|
||||||
print(f" http://{local_ip}:{WEB_PORT}")
|
print(f" http://{local_ip}:{WEB_PORT}")
|
||||||
print(f" http://localhost:{WEB_PORT}")
|
print(f" http://localhost:{WEB_PORT}")
|
||||||
print("="*60 + "\n")
|
print("="*60 + "\n")
|
||||||
|
|
||||||
# Start Flask web server
|
# Start production WSGI server (waitress)
|
||||||
app.run(host='0.0.0.0', port=WEB_PORT, debug=False, threaded=True)
|
print(f"Starting production server on 0.0.0.0:{WEB_PORT}")
|
||||||
|
serve(app, host='0.0.0.0', port=WEB_PORT, threads=4)
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
|||||||
Reference in New Issue
Block a user