diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..5dd1185 --- /dev/null +++ b/CLAUDE.md @@ -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. diff --git a/Makefile b/Makefile index 59f848e..465b9f6 100644 --- a/Makefile +++ b/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!" diff --git a/README.md b/README.md index 54099f6..5cd8e0c 100644 --- a/README.md +++ b/README.md @@ -73,6 +73,7 @@ Web interface available at: `http://: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://: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 diff --git a/pyproject.toml b/pyproject.toml index 63c2006..74220f2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,6 +9,7 @@ dependencies = [ "numpy>=1.21.0", "pyaudio>=0.2.13", "RPi.GPIO>=0.7.1", + "waitress>=2.1.0", ] [project.scripts] diff --git a/rotary_phone_web.py b/rotary_phone_web.py index 68ffd59..9db87c3 100644 --- a/rotary_phone_web.py +++ b/rotary_phone_web.py @@ -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()