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:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -44,6 +44,7 @@ sounds/
|
|||||||
|
|
||||||
# Configuration
|
# Configuration
|
||||||
config.json
|
config.json
|
||||||
|
user_config.json
|
||||||
*.backup
|
*.backup
|
||||||
|
|
||||||
# Logs
|
# Logs
|
||||||
|
|||||||
138
README.md
138
README.md
@@ -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
27
config.example.json
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -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"""
|
||||||
|
|||||||
Reference in New Issue
Block a user