Add external configuration file system

Breaking Changes:
- Configuration now via config.json instead of editing Python code
- Remove all hardcoded paths (no more /home/berwn)
- Separate system config (config.json) from runtime config (user_config.json)

Features:
- config.example.json with all configurable options
- GPIO pin and state configuration
- Audio device index configuration
- Customizable paths (relative or absolute)
- Web port and upload size settings
- No code editing required for deployment

Configuration Structure:
- gpio: hook_pin, hook_pressed_state
- audio: device_index, chunk_size, channels, sample_rate, max_record_seconds
- paths: base_dir, recordings_dir, sounds_dir
- web: port, max_upload_size_mb
- system: active_greeting, default_volume

Script automatically:
- Checks for config.json on startup
- Provides helpful error if missing
- Uses relative paths by default
- Loads test_complete.py config from same file

Updated Documentation:
- Complete configuration guide in README
- Setup instructions without hardcoded paths
- Troubleshooting for config errors
- Device index discovery command

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-10-24 14:48:43 +07:00
parent 1657a242fd
commit ef0373e60b
5 changed files with 196 additions and 74 deletions

1
.gitignore vendored
View File

@@ -44,6 +44,7 @@ sounds/
# Configuration # Configuration
config.json config.json
user_config.json
*.backup *.backup
# Logs # Logs

138
README.md
View File

@@ -86,7 +86,43 @@ chmod +x configure_hifiberry.sh
Or follow the manual instructions in `AUDIO_FIX.md`. 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 ```bash
python3 test_complete.py python3 test_complete.py
@@ -97,15 +133,6 @@ This will test:
- Dial tone generation - Dial tone generation
- Microphone recording - 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 ### 7. Run the System
```bash ```bash
@@ -161,6 +188,7 @@ wedding-phone/
├── rotary_phone_web.py # Main application ├── rotary_phone_web.py # Main application
├── test_complete.py # Audio testing script ├── test_complete.py # Audio testing script
├── configure_hifiberry.sh # HiFiBerry setup script ├── configure_hifiberry.sh # HiFiBerry setup script
├── config.example.json # Example configuration (copy to config.json)
├── pyproject.toml # UV/pip package configuration ├── pyproject.toml # UV/pip package configuration
├── AUDIO_FIX.md # Audio configuration guide ├── AUDIO_FIX.md # Audio configuration guide
├── README.md # This file ├── README.md # This file
@@ -172,47 +200,59 @@ wedding-phone/
### Runtime Data (Auto-created) ### Runtime Data (Auto-created)
``` ```
/home/berwn/rotary_phone_data/ rotary_phone_data/ # Default location (configurable)
├── recordings/ # Voice recordings from guests ├── recordings/ # Voice recordings from guests
├── sounds/ # Greeting message WAV files ├── sounds/ # Greeting message WAV files
└── config.json # Active greeting configuration └── user_config.json # Runtime settings (volume, active greeting)
``` ```
## Configuration ## 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 The `config.json` file contains all system settings:
CHUNK = 1024 # Audio buffer size
FORMAT = pyaudio.paInt16 # 16-bit audio ```json
CHANNELS = 1 # Mono {
RATE = 44100 # 44.1kHz sample rate "gpio": {
RECORD_SECONDS = 300 # Max recording time (5 minutes) "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: To find your HiFiBerry or other audio 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 ```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())]" 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 Look for your HiFiBerry device and note its index number, then set it in `config.json`.
```python
WEB_PORT = 8080 # Change to your preferred port
```
## Troubleshooting ## Troubleshooting
@@ -249,9 +289,10 @@ WEB_PORT = 8080 # Change to your preferred port
### GPIO Not Detecting Hookswitch ### 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 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 ```python
import RPi.GPIO as GPIO import RPi.GPIO as GPIO
GPIO.setmode(GPIO.BCM) GPIO.setmode(GPIO.BCM)
@@ -262,9 +303,18 @@ WEB_PORT = 8080 # Change to your preferred port
### Web Interface Not Accessible ### Web Interface Not Accessible
1. Check if Flask is running: `ps aux | grep python` 1. Check if Flask is running: `ps aux | grep python`
2. Verify firewall: `sudo ufw allow 8080` 2. Verify port in `config.json` matches URL
3. Check IP address: `hostname -I` 3. Check firewall: `sudo ufw allow 8080` (or your configured port)
4. Try localhost: `http://127.0.0.1:8080` 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 ## Auto-start on Boot
@@ -274,7 +324,7 @@ Create a systemd service:
sudo nano /etc/systemd/system/wedding-phone.service sudo nano /etc/systemd/system/wedding-phone.service
``` ```
Add: Add (replace `/path/to/wedding-phone` with your actual path):
```ini ```ini
[Unit] [Unit]
@@ -283,9 +333,9 @@ After=network.target sound.target
[Service] [Service]
Type=simple Type=simple
User=berwn User=pi
WorkingDirectory=/home/berwn/wedding-phone WorkingDirectory=/path/to/wedding-phone
ExecStart=/usr/bin/python3 /home/berwn/wedding-phone/rotary_phone_web.py ExecStart=/usr/bin/python3 /path/to/wedding-phone/rotary_phone_web.py
Restart=always Restart=always
RestartSec=10 RestartSec=10

27
config.example.json Normal file
View File

@@ -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
}
}

View File

@@ -15,31 +15,57 @@ 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
import json import json
import sys
# Configuration # Load configuration
HOOK_PIN = 17 # GPIO pin for hook switch (change to your pin) def load_system_config():
HOOK_PRESSED = GPIO.LOW # Change to GPIO.HIGH if your switch is active high """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 # Audio settings
CHUNK = 1024 AUDIO_DEVICE_INDEX = SYS_CONFIG['audio']['device_index']
FORMAT = pyaudio.paInt16 CHUNK = SYS_CONFIG['audio']['chunk_size']
CHANNELS = 1 FORMAT = pyaudio.paInt16 # Fixed format
RATE = 44100 CHANNELS = SYS_CONFIG['audio']['channels']
RECORD_SECONDS = 300 # Maximum recording time (5 minutes) RATE = SYS_CONFIG['audio']['sample_rate']
RECORD_SECONDS = SYS_CONFIG['audio']['max_record_seconds']
# Directories # Directories
BASE_DIR = "/home/berwn/rotary_phone_data" SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
OUTPUT_DIR = os.path.join(BASE_DIR, "recordings") 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']
SOUNDS_DIR = os.path.join(BASE_DIR, "sounds") BASE_DIR = os.path.abspath(BASE_DIR)
CONFIG_FILE = os.path.join(BASE_DIR, "config.json") OUTPUT_DIR = os.path.join(BASE_DIR, SYS_CONFIG['paths']['recordings_dir'])
DIALTONE_FILE = os.path.join(SOUNDS_DIR, "dialtone.wav") # Legacy default 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 server settings
WEB_PORT = 8080 WEB_PORT = SYS_CONFIG['web']['port']
# Flask app # Flask app
app = Flask(__name__) 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: class RotaryPhone:
def __init__(self): def __init__(self):
@@ -64,20 +90,20 @@ class RotaryPhone:
self.generate_default_dialtone() self.generate_default_dialtone()
def load_config(self): def load_config(self):
"""Load configuration from JSON file""" """Load user runtime configuration from JSON file"""
default_config = { default_config = {
"active_greeting": "dialtone.wav", "active_greeting": SYS_CONFIG['system']['active_greeting'],
"greetings": [], "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: try:
with open(CONFIG_FILE, 'r') as f: with open(USER_CONFIG_FILE, 'r') as f:
config = json.load(f) config = json.load(f)
# Ensure volume key exists # Ensure volume key exists
if "volume" not in config: if "volume" not in config:
config["volume"] = 70 config["volume"] = SYS_CONFIG['system']['volume']
return config return config
except: except:
pass pass
@@ -85,8 +111,8 @@ class RotaryPhone:
return default_config return default_config
def save_config(self): def save_config(self):
"""Save configuration to JSON file""" """Save user runtime configuration to JSON file"""
with open(CONFIG_FILE, 'w') as f: with open(USER_CONFIG_FILE, 'w') as f:
json.dump(self.config, f, indent=2) json.dump(self.config, f, indent=2)
def get_active_greeting_path(self): def get_active_greeting_path(self):
@@ -145,13 +171,13 @@ class RotaryPhone:
try: try:
wf = wave.open(filepath, 'rb') wf = wave.open(filepath, 'rb')
# Use device index 1 for HiFiBerry (change if needed) # Use configured audio device
stream = self.audio.open( stream = self.audio.open(
format=self.audio.get_format_from_width(wf.getsampwidth()), format=self.audio.get_format_from_width(wf.getsampwidth()),
channels=wf.getnchannels(), channels=wf.getnchannels(),
rate=wf.getframerate(), rate=wf.getframerate(),
output=True, output=True,
output_device_index=1, # HiFiBerry device index output_device_index=AUDIO_DEVICE_INDEX,
frames_per_buffer=CHUNK frames_per_buffer=CHUNK
) )
@@ -189,13 +215,13 @@ class RotaryPhone:
print(f"Recording to {filename}") print(f"Recording to {filename}")
try: try:
# Use device index 1 for HiFiBerry (change if needed) # Use configured audio device
stream = self.audio.open( stream = self.audio.open(
format=FORMAT, format=FORMAT,
channels=CHANNELS, channels=CHANNELS,
rate=RATE, rate=RATE,
input=True, input=True,
input_device_index=1, # HiFiBerry device index input_device_index=AUDIO_DEVICE_INDEX,
frames_per_buffer=CHUNK frames_per_buffer=CHUNK
) )

View File

@@ -8,10 +8,28 @@ import wave
import numpy as np import numpy as np
import time import time
import os import os
import json
import sys
# HiFiBerry device index (from your system) # Load configuration
HIFIBERRY_INDEX = 1 def load_config():
SAMPLE_RATE = 44100 """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(): def test_playback():
"""Test speaker output""" """Test speaker output"""