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:
|
||||
@echo "Installing/syncing dependencies..."
|
||||
uv pip install flask numpy pyaudio RPi.GPIO
|
||||
uv pip install flask numpy pyaudio RPi.GPIO waitress
|
||||
|
||||
install: sync
|
||||
@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
|
||||
- **Data Integrity**: Every file write is verified with CRC checksums
|
||||
- **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)
|
||||
- **Auto-refresh**: Status updates every 5 seconds
|
||||
|
||||
@@ -95,6 +96,7 @@ Web interface available at: `http://<raspberry-pi-ip>:8080`
|
||||
- numpy>=1.21.0
|
||||
- pyaudio>=0.2.13
|
||||
- RPi.GPIO>=0.7.1
|
||||
- waitress>=2.1.0 (production WSGI server)
|
||||
|
||||
## Installation
|
||||
|
||||
@@ -127,7 +129,7 @@ sudo apt-get install -y python3-pyaudio portaudio19-dev
|
||||
# Install Python dependencies with UV
|
||||
make sync
|
||||
# Or manually:
|
||||
# uv pip install flask numpy pyaudio RPi.GPIO
|
||||
# uv pip install flask numpy pyaudio RPi.GPIO waitress
|
||||
```
|
||||
|
||||
#### Option B: Using pip
|
||||
@@ -135,7 +137,7 @@ make sync
|
||||
```bash
|
||||
sudo apt-get update
|
||||
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
|
||||
|
||||
@@ -9,6 +9,7 @@ dependencies = [
|
||||
"numpy>=1.21.0",
|
||||
"pyaudio>=0.2.13",
|
||||
"RPi.GPIO>=0.7.1",
|
||||
"waitress>=2.1.0",
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
|
||||
@@ -14,6 +14,7 @@ import numpy as np
|
||||
import threading
|
||||
from flask import Flask, render_template, request, send_file, jsonify, redirect, url_for
|
||||
from werkzeug.utils import secure_filename
|
||||
from waitress import serve
|
||||
import json
|
||||
import sys
|
||||
import zlib
|
||||
@@ -1989,13 +1990,15 @@ def main():
|
||||
# Get and display local IP
|
||||
local_ip = get_local_ip()
|
||||
print("\n" + "="*60)
|
||||
print(f"🚀 Wedding Phone System Starting...")
|
||||
print(f"Web Interface Available At:")
|
||||
print(f" http://{local_ip}:{WEB_PORT}")
|
||||
print(f" http://localhost:{WEB_PORT}")
|
||||
print("="*60 + "\n")
|
||||
|
||||
# Start Flask web server
|
||||
app.run(host='0.0.0.0', port=WEB_PORT, debug=False, threaded=True)
|
||||
|
||||
# Start production WSGI server (waitress)
|
||||
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__":
|
||||
main()
|
||||
|
||||
Reference in New Issue
Block a user