diff --git a/.gitignore b/.gitignore index 5d21ac8..40ebd5f 100644 --- a/.gitignore +++ b/.gitignore @@ -44,6 +44,7 @@ sounds/ # Configuration config.json +user_config.json *.backup # Logs diff --git a/README.md b/README.md index a8c06f5..7eda059 100644 --- a/README.md +++ b/README.md @@ -86,7 +86,43 @@ chmod +x configure_hifiberry.sh Or follow the manual instructions in `AUDIO_FIX.md`. -### 5. Test Your Audio +### 5. Create Configuration File + +Copy the example configuration and customize it: + +```bash +cp config.example.json config.json +nano config.json # or use your preferred editor +``` + +**Important settings to configure:** + +```json +{ + "gpio": { + "hook_pin": 17, // GPIO pin for hookswitch + "hook_pressed_state": "LOW" // "LOW" or "HIGH" + }, + "audio": { + "device_index": 1, // HiFiBerry device index + "sample_rate": 44100 + }, + "paths": { + "base_dir": "./rotary_phone_data" // Relative or absolute path + }, + "web": { + "port": 8080 + } +} +``` + +**Finding your 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())]" +``` + +### 6. Test Your Audio ```bash python3 test_complete.py @@ -97,15 +133,6 @@ This will test: - Dial tone generation - Microphone recording -### 6. 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 -``` - ### 7. Run the System ```bash @@ -161,6 +188,7 @@ wedding-phone/ ├── rotary_phone_web.py # Main application ├── test_complete.py # Audio testing script ├── configure_hifiberry.sh # HiFiBerry setup script +├── config.example.json # Example configuration (copy to config.json) ├── pyproject.toml # UV/pip package configuration ├── AUDIO_FIX.md # Audio configuration guide ├── README.md # This file @@ -172,47 +200,59 @@ wedding-phone/ ### Runtime Data (Auto-created) ``` -/home/berwn/rotary_phone_data/ +rotary_phone_data/ # Default location (configurable) ├── recordings/ # Voice recordings from guests ├── sounds/ # Greeting message WAV files -└── config.json # Active greeting configuration +└── user_config.json # Runtime settings (volume, active greeting) ``` ## Configuration -### Audio Settings +All configuration is done via the `config.json` file. **No need to edit Python code!** -Edit these constants in `rotary_phone_web.py`: +### Configuration File Structure -```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) +The `config.json` file contains all system settings: + +```json +{ + "gpio": { + "hook_pin": 17, // GPIO pin number for hookswitch + "hook_pressed_state": "LOW" // "LOW" or "HIGH" depending on switch + }, + "audio": { + "device_index": 1, // Audio device index (run test to find) + "chunk_size": 1024, // Audio buffer size + "format": "paInt16", // Audio format (16-bit) + "channels": 1, // Mono audio + "sample_rate": 44100, // 44.1kHz sample rate + "max_record_seconds": 300 // Max recording time (5 minutes) + }, + "paths": { + "base_dir": "./rotary_phone_data", // Data directory (relative or absolute) + "recordings_dir": "recordings", // Subdirectory for recordings + "sounds_dir": "sounds" // Subdirectory for greeting sounds + }, + "web": { + "port": 8080, // Web interface port + "max_upload_size_mb": 50 // Max upload file size + }, + "system": { + "active_greeting": "dialtone.wav", // Default greeting + "volume": 70 // Default volume (0-100) + } +} ``` -### HiFiBerry Device Index +### Finding Your Audio Device -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: +To find your HiFiBerry or other 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())]" ``` -### Web Server Port - -```python -WEB_PORT = 8080 # Change to your preferred port -``` +Look for your HiFiBerry device and note its index number, then set it in `config.json`. ## Troubleshooting @@ -249,9 +289,10 @@ WEB_PORT = 8080 # Change to your preferred port ### GPIO Not Detecting Hookswitch -1. Verify GPIO pin number +1. Verify GPIO pin number in `config.json` 2. Check if switch is normally open or closed -3. Test with a simple script: +3. Update `hook_pressed_state` in `config.json` ("LOW" or "HIGH") +4. Test with a simple script: ```python import RPi.GPIO as GPIO GPIO.setmode(GPIO.BCM) @@ -262,9 +303,18 @@ WEB_PORT = 8080 # Change to your preferred port ### 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` +2. Verify port in `config.json` matches URL +3. Check firewall: `sudo ufw allow 8080` (or your configured port) +4. Check IP address: `hostname -I` +5. Try localhost: `http://127.0.0.1:8080` + +### Configuration Errors + +If the script won't start: +1. Ensure `config.json` exists (copy from `config.example.json`) +2. Validate JSON syntax: `python3 -m json.tool config.json` +3. Check all paths exist or can be created +4. Verify audio device index is correct ## Auto-start on Boot @@ -274,7 +324,7 @@ Create a systemd service: sudo nano /etc/systemd/system/wedding-phone.service ``` -Add: +Add (replace `/path/to/wedding-phone` with your actual path): ```ini [Unit] @@ -283,9 +333,9 @@ 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 +User=pi +WorkingDirectory=/path/to/wedding-phone +ExecStart=/usr/bin/python3 /path/to/wedding-phone/rotary_phone_web.py Restart=always RestartSec=10 diff --git a/config.example.json b/config.example.json new file mode 100644 index 0000000..fb8a003 --- /dev/null +++ b/config.example.json @@ -0,0 +1,27 @@ +{ + "gpio": { + "hook_pin": 17, + "hook_pressed_state": "LOW" + }, + "audio": { + "device_index": 1, + "chunk_size": 1024, + "format": "paInt16", + "channels": 1, + "sample_rate": 44100, + "max_record_seconds": 300 + }, + "paths": { + "base_dir": "./rotary_phone_data", + "recordings_dir": "recordings", + "sounds_dir": "sounds" + }, + "web": { + "port": 8080, + "max_upload_size_mb": 50 + }, + "system": { + "active_greeting": "dialtone.wav", + "volume": 70 + } +} diff --git a/rotary_phone_web.py b/rotary_phone_web.py index cd52202..23912ea 100644 --- a/rotary_phone_web.py +++ b/rotary_phone_web.py @@ -15,31 +15,57 @@ import threading from flask import Flask, render_template, request, send_file, jsonify, redirect, url_for from werkzeug.utils import secure_filename import json +import sys -# Configuration -HOOK_PIN = 17 # GPIO pin for hook switch (change to your pin) -HOOK_PRESSED = GPIO.LOW # Change to GPIO.HIGH if your switch is active high +# Load configuration +def load_system_config(): + """Load system configuration from config.json""" + config_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'config.json') + + # Check if config exists, otherwise use example + if not os.path.exists(config_path): + example_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'config.example.json') + if os.path.exists(example_path): + print(f"Config file not found. Please copy {example_path} to {config_path}") + print("Command: cp config.example.json config.json") + sys.exit(1) + else: + print("ERROR: No configuration file found!") + sys.exit(1) + + with open(config_path, 'r') as f: + return json.load(f) + +# Load system configuration +SYS_CONFIG = load_system_config() + +# GPIO Configuration +HOOK_PIN = SYS_CONFIG['gpio']['hook_pin'] +HOOK_PRESSED = GPIO.LOW if SYS_CONFIG['gpio']['hook_pressed_state'] == 'LOW' else GPIO.HIGH # Audio settings -CHUNK = 1024 -FORMAT = pyaudio.paInt16 -CHANNELS = 1 -RATE = 44100 -RECORD_SECONDS = 300 # Maximum recording time (5 minutes) +AUDIO_DEVICE_INDEX = SYS_CONFIG['audio']['device_index'] +CHUNK = SYS_CONFIG['audio']['chunk_size'] +FORMAT = pyaudio.paInt16 # Fixed format +CHANNELS = SYS_CONFIG['audio']['channels'] +RATE = SYS_CONFIG['audio']['sample_rate'] +RECORD_SECONDS = SYS_CONFIG['audio']['max_record_seconds'] # Directories -BASE_DIR = "/home/berwn/rotary_phone_data" -OUTPUT_DIR = os.path.join(BASE_DIR, "recordings") -SOUNDS_DIR = os.path.join(BASE_DIR, "sounds") -CONFIG_FILE = os.path.join(BASE_DIR, "config.json") -DIALTONE_FILE = os.path.join(SOUNDS_DIR, "dialtone.wav") # Legacy default +SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) +BASE_DIR = os.path.join(SCRIPT_DIR, SYS_CONFIG['paths']['base_dir']) if SYS_CONFIG['paths']['base_dir'].startswith('.') else SYS_CONFIG['paths']['base_dir'] +BASE_DIR = os.path.abspath(BASE_DIR) +OUTPUT_DIR = os.path.join(BASE_DIR, SYS_CONFIG['paths']['recordings_dir']) +SOUNDS_DIR = os.path.join(BASE_DIR, SYS_CONFIG['paths']['sounds_dir']) +USER_CONFIG_FILE = os.path.join(BASE_DIR, "user_config.json") # Runtime user settings +DIALTONE_FILE = os.path.join(SOUNDS_DIR, "dialtone.wav") # Web server settings -WEB_PORT = 8080 +WEB_PORT = SYS_CONFIG['web']['port'] # Flask app app = Flask(__name__) -app.config['MAX_CONTENT_LENGTH'] = 50 * 1024 * 1024 # 50MB max file size +app.config['MAX_CONTENT_LENGTH'] = SYS_CONFIG['web']['max_upload_size_mb'] * 1024 * 1024 class RotaryPhone: def __init__(self): @@ -64,20 +90,20 @@ class RotaryPhone: self.generate_default_dialtone() def load_config(self): - """Load configuration from JSON file""" + """Load user runtime configuration from JSON file""" default_config = { - "active_greeting": "dialtone.wav", + "active_greeting": SYS_CONFIG['system']['active_greeting'], "greetings": [], - "volume": 70 # Default volume percentage (0-100) + "volume": SYS_CONFIG['system']['volume'] } - if os.path.exists(CONFIG_FILE): + if os.path.exists(USER_CONFIG_FILE): try: - with open(CONFIG_FILE, 'r') as f: + with open(USER_CONFIG_FILE, 'r') as f: config = json.load(f) # Ensure volume key exists if "volume" not in config: - config["volume"] = 70 + config["volume"] = SYS_CONFIG['system']['volume'] return config except: pass @@ -85,8 +111,8 @@ class RotaryPhone: return default_config def save_config(self): - """Save configuration to JSON file""" - with open(CONFIG_FILE, 'w') as f: + """Save user runtime configuration to JSON file""" + with open(USER_CONFIG_FILE, 'w') as f: json.dump(self.config, f, indent=2) def get_active_greeting_path(self): @@ -145,13 +171,13 @@ class RotaryPhone: try: wf = wave.open(filepath, 'rb') - # Use device index 1 for HiFiBerry (change if needed) + # Use configured audio device stream = self.audio.open( format=self.audio.get_format_from_width(wf.getsampwidth()), channels=wf.getnchannels(), rate=wf.getframerate(), output=True, - output_device_index=1, # HiFiBerry device index + output_device_index=AUDIO_DEVICE_INDEX, frames_per_buffer=CHUNK ) @@ -189,13 +215,13 @@ class RotaryPhone: print(f"Recording to {filename}") try: - # Use device index 1 for HiFiBerry (change if needed) + # Use configured audio device stream = self.audio.open( format=FORMAT, channels=CHANNELS, rate=RATE, input=True, - input_device_index=1, # HiFiBerry device index + input_device_index=AUDIO_DEVICE_INDEX, frames_per_buffer=CHUNK ) diff --git a/test_complete.py b/test_complete.py index 657276a..ac984a6 100644 --- a/test_complete.py +++ b/test_complete.py @@ -8,10 +8,28 @@ import wave import numpy as np import time import os +import json +import sys -# HiFiBerry device index (from your system) -HIFIBERRY_INDEX = 1 -SAMPLE_RATE = 44100 +# Load configuration +def load_config(): + """Load audio device configuration""" + config_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'config.json') + + if not os.path.exists(config_path): + print("Config file not found. Using default device index 1") + return {"audio": {"device_index": 1, "sample_rate": 44100}} + + try: + with open(config_path, 'r') as f: + return json.load(f) + except: + print("Error reading config. Using defaults.") + return {"audio": {"device_index": 1, "sample_rate": 44100}} + +CONFIG = load_config() +HIFIBERRY_INDEX = CONFIG['audio']['device_index'] +SAMPLE_RATE = CONFIG['audio']['sample_rate'] def test_playback(): """Test speaker output"""