From 753b1bddfae130dadd217b29087b38b86202717e Mon Sep 17 00:00:00 2001 From: grabowski Date: Fri, 24 Oct 2025 14:39:56 +0700 Subject: [PATCH] Add .gitignore and comprehensive README MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Changes: - Add .gitignore to exclude __pycache__, .claude/, and data directories - Remove cached files and Claude config from repository - Create detailed README.md with: - Project overview and features - Installation and setup instructions - Usage guide for web interface and phone operation - Hardware and software requirements - Troubleshooting section - API documentation - Auto-start configuration 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .claude/settings.local.json | 10 - .gitignore | 53 ++++ README.md | 304 +++++++++++++++++++ __pycache__/rotary_phone_web.cpython-311.pyc | Bin 51238 -> 0 bytes 4 files changed, 357 insertions(+), 10 deletions(-) delete mode 100644 .claude/settings.local.json create mode 100644 .gitignore create mode 100644 README.md delete mode 100644 __pycache__/rotary_phone_web.cpython-311.pyc diff --git a/.claude/settings.local.json b/.claude/settings.local.json deleted file mode 100644 index ded38bf..0000000 --- a/.claude/settings.local.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "permissions": { - "allow": [ - "Bash(python -m py_compile:*)", - "Bash(git add:*)" - ], - "deny": [], - "ask": [] - } -} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5d21ac8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,53 @@ +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg + +# Virtual environments +venv/ +env/ +ENV/ +.venv + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ +.DS_Store + +# Claude Code +.claude/ + +# Data directories (runtime data) +rotary_phone_data/ +recordings/ +sounds/ + +# Configuration +config.json +*.backup + +# Logs +*.log + +# OS +Thumbs.db diff --git a/README.md b/README.md new file mode 100644 index 0000000..1c8e91d --- /dev/null +++ b/README.md @@ -0,0 +1,304 @@ +# Wedding Phone - Vintage Rotary Phone Audio System + +A Raspberry Pi-based rotary phone system for weddings and events. Guests can pick up the handset to hear custom greeting messages and leave voice recordings. Features a modern web interface for managing messages and recordings. + +## Features + +- **Vintage Phone Integration**: Uses a real rotary phone with GPIO hook detection +- **Custom Greeting Messages**: Upload multiple greeting messages and select which one plays +- **Voice Recording**: Automatically records guest messages after the greeting +- **Web Interface**: Beautiful, responsive web UI for managing the system +- **Audio Playback**: Play recordings and greetings directly in the browser +- **Multiple Message Support**: Upload and manage multiple greeting messages +- **Active Message Selector**: Choose which greeting plays when the phone is picked up +- **HiFiBerry Support**: Optimized for HiFiBerry DAC+ADC Pro audio quality +- **Real-time Status**: Monitor phone status (on-hook/off-hook/recording) +- **Auto-refresh**: Status updates every 5 seconds + +## Hardware Requirements + +- Raspberry Pi (3/4/5 or Zero 2 W) +- HiFiBerry DAC+ADC Pro (or similar audio interface) +- Vintage rotary phone with hookswitch +- Speaker (connected to HiFiBerry output) +- Microphone (connected to HiFiBerry input, or use phone handset mic) + +## Software Requirements + +- Raspberry Pi OS (Bullseye or newer) +- Python 3.7+ +- Required Python packages: + - pyaudio + - flask + - numpy + - RPi.GPIO + - wave (built-in) + +## Installation + +### 1. Clone the Repository + +```bash +git clone https://git.b4l.co.th/grabowski/wedding-phone.git +cd wedding-phone +``` + +### 2. Install Dependencies + +```bash +sudo apt-get update +sudo apt-get install -y python3-pip python3-pyaudio portaudio19-dev +pip3 install flask numpy RPi.GPIO +``` + +### 3. Configure HiFiBerry + +Run the automatic configuration script: + +```bash +chmod +x configure_hifiberry.sh +./configure_hifiberry.sh +``` + +Or follow the manual instructions in `AUDIO_FIX.md`. + +### 4. Test Your Audio + +```bash +python3 test_complete.py +``` + +This will test: +- Speaker playback +- Dial tone generation +- Microphone recording + +### 5. Configure GPIO Pin + +Edit `rotary_phone_web.py` and set your hookswitch GPIO pin: + +```python +HOOK_PIN = 17 # Change to your GPIO pin number +HOOK_PRESSED = GPIO.LOW # Or GPIO.HIGH depending on your switch +``` + +### 6. Run the System + +```bash +python3 rotary_phone_web.py +``` + +The web interface will be available at: +- `http://localhost:8080` +- `http://:8080` + +## Usage + +### Web Interface + +The web interface provides three main sections: + +#### 1. Phone Status +- Shows current phone state (on-hook/off-hook/recording) +- Displays active recording filename +- Auto-refreshes every 5 seconds + +#### 2. Greeting Messages +- **Upload**: Click "Choose WAV File(s)" to upload one or multiple greeting messages +- **Play**: Click "▶️ Play" to preview any greeting in your browser +- **Set Active**: Click "⭐ Set Active" to select which greeting plays when the phone is picked up +- **Delete**: Remove unwanted greetings (cannot delete the active one) +- **Default Tone**: Generate a classic telephone dial tone + +#### 3. Recordings +- **Play**: Listen to recordings directly in the browser +- **Download**: Save recordings to your computer +- **Delete**: Remove unwanted recordings +- **Statistics**: View total recordings, storage used, and total duration + +### Phone Operation + +1. **Guest picks up phone**: System detects via GPIO +2. **Greeting plays**: Active greeting message plays through speaker +3. **Recording starts**: After greeting, system records guest message +4. **Guest hangs up**: Recording stops and saves automatically +5. **Ready for next call**: System returns to waiting state + +## File Structure + +``` +wedding-phone/ +├── rotary_phone_web.py # Main application +├── test_complete.py # Audio testing script +├── configure_hifiberry.sh # HiFiBerry setup script +├── AUDIO_FIX.md # Audio configuration guide +├── README.md # This file +├── .gitignore # Git ignore rules +└── templates/ # Auto-generated on first run + └── index.html # Web interface (embedded in script) +``` + +### Runtime Data (Auto-created) + +``` +/home/berwn/rotary_phone_data/ +├── recordings/ # Voice recordings from guests +├── sounds/ # Greeting message WAV files +└── config.json # Active greeting configuration +``` + +## Configuration + +### Audio Settings + +Edit these constants in `rotary_phone_web.py`: + +```python +CHUNK = 1024 # Audio buffer size +FORMAT = pyaudio.paInt16 # 16-bit audio +CHANNELS = 1 # Mono +RATE = 44100 # 44.1kHz sample rate +RECORD_SECONDS = 300 # Max recording time (5 minutes) +``` + +### HiFiBerry Device Index + +If your HiFiBerry is at a different device index: + +```python +# Line ~103 and ~138 in rotary_phone_web.py +output_device_index=1, # Change this number +input_device_index=1, # Change this number +``` + +Find your 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())]" +``` + +### Web Server Port + +```python +WEB_PORT = 8080 # Change to your preferred port +``` + +## Troubleshooting + +### No Sound from Speaker + +1. Check HiFiBerry configuration: + ```bash + aplay -l # List audio devices + amixer -c 3 sset Digital 100% # Set volume (adjust card number) + ``` + +2. Test speaker directly: + ```bash + speaker-test -D plughw:3,0 -c 1 -t wav + ``` + +3. Run the complete test: + ```bash + python3 test_complete.py + ``` + +### Microphone Not Recording + +1. Check microphone is connected to HiFiBerry input +2. Adjust input gain: + ```bash + alsamixer -c 3 + ``` +3. Test recording: + ```bash + arecord -D plughw:3,0 -f cd test.wav -d 5 + aplay test.wav + ``` + +### GPIO Not Detecting Hookswitch + +1. Verify GPIO pin number +2. Check if switch is normally open or closed +3. Test with a simple script: + ```python + import RPi.GPIO as GPIO + GPIO.setmode(GPIO.BCM) + GPIO.setup(17, GPIO.IN, pull_up_down=GPIO.PUD_UP) + print(GPIO.input(17)) # Should change when switch toggles + ``` + +### Web Interface Not Accessible + +1. Check if Flask is running: `ps aux | grep python` +2. Verify firewall: `sudo ufw allow 8080` +3. Check IP address: `hostname -I` +4. Try localhost: `http://127.0.0.1:8080` + +## Auto-start on Boot + +Create a systemd service: + +```bash +sudo nano /etc/systemd/system/wedding-phone.service +``` + +Add: + +```ini +[Unit] +Description=Wedding Phone Service +After=network.target sound.target + +[Service] +Type=simple +User=berwn +WorkingDirectory=/home/berwn/wedding-phone +ExecStart=/usr/bin/python3 /home/berwn/wedding-phone/rotary_phone_web.py +Restart=always +RestartSec=10 + +[Install] +WantedBy=multi-user.target +``` + +Enable and start: + +```bash +sudo systemctl daemon-reload +sudo systemctl enable wedding-phone.service +sudo systemctl start wedding-phone.service +sudo systemctl status wedding-phone.service +``` + +## API Endpoints + +The system provides REST API endpoints: + +- `GET /` - Web interface +- `GET /api/status` - Phone status JSON +- `GET /api/recordings` - List all recordings +- `GET /api/greetings` - List all greeting messages +- `POST /upload_greeting` - Upload new greeting +- `POST /set_active_greeting` - Set active greeting +- `POST /delete_greeting/` - Delete greeting +- `GET /play_audio//` - Stream audio file +- `GET /download/` - Download recording +- `POST /delete/` - Delete recording +- `POST /restore_default_sound` - Generate default dial tone + +## Contributing + +Feel free to open issues or submit pull requests for improvements. + +## License + +This project is open source and available for personal and commercial use. + +## Credits + +Created for wedding events to capture guest messages in a unique, nostalgic way. + +## Support + +For issues or questions, please open an issue on the repository. diff --git a/__pycache__/rotary_phone_web.cpython-311.pyc b/__pycache__/rotary_phone_web.cpython-311.pyc deleted file mode 100644 index 6f9c1dc12778b829361d86c26abf87822eb399eb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 51238 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