Add extra GPIO button support with custom sound playback
Features: - Configurable extra button on any GPIO pin - Upload and select custom sound for button press - Non-blocking button operation (separate thread) - Debounced to prevent double-triggers (0.5s) - Works independently from phone handset - Can be pressed anytime, even during recording Configuration: - gpio.extra_button_enabled: Enable/disable feature - gpio.extra_button_pin: GPIO pin number (default: 27) - gpio.extra_button_pressed_state: "LOW" or "HIGH" - system.extra_button_sound: Default sound file Web Interface: - Display active button sound in alert - "🔘 Set Button" action on greeting items - Visual indicator (🔘) for active button sound - Upload any WAV file and assign to button - Play/preview button sounds in browser Backend: - RotaryPhone.play_extra_button_sound() method - RotaryPhone.set_extra_button_sound() method - Thread-based playback to not block main loop - /set_extra_button_sound API endpoint - Extra button sound tracked in user_config.json Documentation: - Extra button setup in README - Configuration examples - GPIO pin configuration - Operation workflow Use Cases: - Play special message on button press - Sound effects for wedding games - Multiple interaction points - Custom audio triggers 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -43,6 +43,11 @@ SYS_CONFIG = load_system_config()
|
||||
HOOK_PIN = SYS_CONFIG['gpio']['hook_pin']
|
||||
HOOK_PRESSED = GPIO.LOW if SYS_CONFIG['gpio']['hook_pressed_state'] == 'LOW' else GPIO.HIGH
|
||||
|
||||
# Extra Button Configuration
|
||||
EXTRA_BUTTON_ENABLED = SYS_CONFIG['gpio'].get('extra_button_enabled', False)
|
||||
EXTRA_BUTTON_PIN = SYS_CONFIG['gpio'].get('extra_button_pin', 27)
|
||||
EXTRA_BUTTON_PRESSED = GPIO.LOW if SYS_CONFIG['gpio'].get('extra_button_pressed_state', 'LOW') == 'LOW' else GPIO.HIGH
|
||||
|
||||
# Audio settings
|
||||
AUDIO_DEVICE_INDEX = SYS_CONFIG['audio']['device_index']
|
||||
CHUNK = SYS_CONFIG['audio']['chunk_size']
|
||||
@@ -78,6 +83,11 @@ class RotaryPhone:
|
||||
GPIO.setmode(GPIO.BCM)
|
||||
GPIO.setup(HOOK_PIN, GPIO.IN, pull_up_down=GPIO.PUD_UP)
|
||||
|
||||
# Setup extra button if enabled
|
||||
if EXTRA_BUTTON_ENABLED:
|
||||
GPIO.setup(EXTRA_BUTTON_PIN, GPIO.IN, pull_up_down=GPIO.PUD_UP)
|
||||
self.extra_button_playing = False
|
||||
|
||||
# Create directories
|
||||
os.makedirs(OUTPUT_DIR, exist_ok=True)
|
||||
os.makedirs(SOUNDS_DIR, exist_ok=True)
|
||||
@@ -93,6 +103,7 @@ class RotaryPhone:
|
||||
"""Load user runtime configuration from JSON file"""
|
||||
default_config = {
|
||||
"active_greeting": SYS_CONFIG['system']['active_greeting'],
|
||||
"extra_button_sound": SYS_CONFIG['system'].get('extra_button_sound', 'button_sound.wav'),
|
||||
"greetings": [],
|
||||
"volume": SYS_CONFIG['system']['volume']
|
||||
}
|
||||
@@ -125,6 +136,16 @@ class RotaryPhone:
|
||||
self.config["active_greeting"] = filename
|
||||
self.save_config()
|
||||
|
||||
def get_extra_button_sound_path(self):
|
||||
"""Get the full path to the extra button sound file"""
|
||||
sound = self.config.get("extra_button_sound", "button_sound.wav")
|
||||
return os.path.join(SOUNDS_DIR, sound)
|
||||
|
||||
def set_extra_button_sound(self, filename):
|
||||
"""Set which sound plays for extra button"""
|
||||
self.config["extra_button_sound"] = filename
|
||||
self.save_config()
|
||||
|
||||
def set_volume(self, volume):
|
||||
"""Set playback volume (0-100)"""
|
||||
volume = max(0, min(100, int(volume))) # Clamp between 0-100
|
||||
@@ -273,14 +294,39 @@ class RotaryPhone:
|
||||
"current_recording": self.current_recording
|
||||
}
|
||||
|
||||
def play_extra_button_sound(self):
|
||||
"""Play sound when extra button is pressed"""
|
||||
if not EXTRA_BUTTON_ENABLED:
|
||||
return
|
||||
|
||||
button_sound = self.get_extra_button_sound_path()
|
||||
if not os.path.exists(button_sound):
|
||||
print(f"Extra button sound not found: {button_sound}")
|
||||
return
|
||||
|
||||
print(f"\n=== Extra button pressed ===")
|
||||
self.extra_button_playing = True
|
||||
self.play_sound_file(button_sound)
|
||||
self.extra_button_playing = False
|
||||
|
||||
def phone_loop(self):
|
||||
"""Main phone handling loop"""
|
||||
print("Rotary Phone System Started")
|
||||
print(f"Hook pin: GPIO {HOOK_PIN}")
|
||||
if EXTRA_BUTTON_ENABLED:
|
||||
print(f"Extra button: GPIO {EXTRA_BUTTON_PIN}")
|
||||
print(f"Recordings will be saved to: {OUTPUT_DIR}")
|
||||
|
||||
|
||||
try:
|
||||
while True:
|
||||
# Check extra button first (higher priority)
|
||||
if EXTRA_BUTTON_ENABLED and GPIO.input(EXTRA_BUTTON_PIN) == EXTRA_BUTTON_PRESSED:
|
||||
if not self.extra_button_playing:
|
||||
# Play extra button sound in a separate thread to not block
|
||||
button_thread = threading.Thread(target=self.play_extra_button_sound, daemon=True)
|
||||
button_thread.start()
|
||||
time.sleep(0.5) # Debounce
|
||||
|
||||
# Wait for handset pickup
|
||||
if GPIO.input(HOOK_PIN) == HOOK_PRESSED and self.phone_status == "on_hook":
|
||||
self.phone_status = "off_hook"
|
||||
@@ -289,13 +335,13 @@ class RotaryPhone:
|
||||
# Play active greeting message
|
||||
greeting_file = self.get_active_greeting_path()
|
||||
self.play_sound_file(greeting_file)
|
||||
|
||||
|
||||
# Start recording
|
||||
if self.phone_status == "off_hook": # Still off hook after sound
|
||||
self.record_audio()
|
||||
|
||||
|
||||
self.phone_status = "on_hook"
|
||||
|
||||
|
||||
time.sleep(0.1)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
@@ -320,12 +366,15 @@ def index():
|
||||
greetings = get_greetings()
|
||||
status = phone.get_status()
|
||||
active_greeting = phone.config.get("active_greeting", "dialtone.wav")
|
||||
extra_button_sound = phone.config.get("extra_button_sound", "button_sound.wav")
|
||||
volume = phone.get_volume()
|
||||
|
||||
return render_template('index.html',
|
||||
recordings=recordings,
|
||||
greetings=greetings,
|
||||
active_greeting=active_greeting,
|
||||
extra_button_sound=extra_button_sound,
|
||||
extra_button_enabled=EXTRA_BUTTON_ENABLED,
|
||||
status=status,
|
||||
volume=volume)
|
||||
|
||||
@@ -401,6 +450,22 @@ def set_active_greeting():
|
||||
phone.set_active_greeting(filename)
|
||||
return jsonify({"success": True, "message": f"Active greeting set to '{filename}'"})
|
||||
|
||||
@app.route('/set_extra_button_sound', methods=['POST'])
|
||||
def set_extra_button_sound():
|
||||
"""Set which sound plays for extra button"""
|
||||
data = request.get_json()
|
||||
filename = data.get('filename')
|
||||
|
||||
if not filename:
|
||||
return jsonify({"error": "No filename provided"}), 400
|
||||
|
||||
filepath = os.path.join(SOUNDS_DIR, filename)
|
||||
if not os.path.exists(filepath):
|
||||
return jsonify({"error": "Sound file not found"}), 404
|
||||
|
||||
phone.set_extra_button_sound(filename)
|
||||
return jsonify({"success": True, "message": f"Extra button sound set to '{filename}'"})
|
||||
|
||||
@app.route('/delete_greeting/<filename>', methods=['POST'])
|
||||
def delete_greeting(filename):
|
||||
"""Delete a greeting file"""
|
||||
@@ -484,7 +549,8 @@ def get_greetings():
|
||||
"size_mb": stat.st_size / (1024 * 1024),
|
||||
"date": datetime.fromtimestamp(stat.st_mtime).strftime('%Y-%m-%d %H:%M:%S'),
|
||||
"duration": duration,
|
||||
"is_active": filename == phone.config.get("active_greeting", "dialtone.wav")
|
||||
"is_active": filename == phone.config.get("active_greeting", "dialtone.wav"),
|
||||
"is_button_sound": filename == phone.config.get("extra_button_sound", "button_sound.wav")
|
||||
})
|
||||
return greetings
|
||||
|
||||
@@ -1000,6 +1066,9 @@ if __name__ == "__main__":
|
||||
|
||||
<div class="alert alert-info">
|
||||
ℹ️ Active greeting: <strong>{{ active_greeting }}</strong>
|
||||
{% if extra_button_enabled %}
|
||||
<br>🔘 Extra button sound: <strong>{{ extra_button_sound }}</strong>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="upload-section">
|
||||
@@ -1032,7 +1101,9 @@ if __name__ == "__main__":
|
||||
<div class="recording-item {% if greeting.is_active %}active-greeting{% endif %}" id="greeting-{{ loop.index }}">
|
||||
<div class="recording-info">
|
||||
<h3>
|
||||
{% if greeting.is_active %}⭐{% endif %} {{ greeting.filename }}
|
||||
{% if greeting.is_active %}⭐{% endif %}
|
||||
{% if greeting.is_button_sound and extra_button_enabled %}🔘{% endif %}
|
||||
{{ greeting.filename }}
|
||||
</h3>
|
||||
<div class="recording-meta">
|
||||
📅 {{ greeting.date }} |
|
||||
@@ -1048,6 +1119,11 @@ if __name__ == "__main__":
|
||||
<button class="btn btn-primary" onclick="setActiveGreeting('{{ greeting.filename }}')">
|
||||
⭐ Set Active
|
||||
</button>
|
||||
{% if extra_button_enabled and not greeting.is_button_sound %}
|
||||
<button class="btn btn-primary" onclick="setExtraButtonSound('{{ greeting.filename }}')">
|
||||
🔘 Set Button
|
||||
</button>
|
||||
{% endif %}
|
||||
<button class="btn btn-danger" onclick="deleteGreeting('{{ greeting.filename }}', {{ loop.index }})">
|
||||
🗑️ Delete
|
||||
</button>
|
||||
@@ -1055,6 +1131,11 @@ if __name__ == "__main__":
|
||||
<button class="btn btn-secondary" disabled>
|
||||
✓ Active
|
||||
</button>
|
||||
{% if extra_button_enabled and not greeting.is_button_sound %}
|
||||
<button class="btn btn-primary" onclick="setExtraButtonSound('{{ greeting.filename }}')">
|
||||
🔘 Set Button
|
||||
</button>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
@@ -1304,6 +1385,26 @@ if __name__ == "__main__":
|
||||
.catch(error => showAlert('Error: ' + error, 'error'));
|
||||
}
|
||||
|
||||
function setExtraButtonSound(filename) {
|
||||
fetch('/set_extra_button_sound', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ filename: filename })
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
showAlert(`Button sound set to '${filename}'! ✓`, 'success');
|
||||
setTimeout(() => location.reload(), 1500);
|
||||
} else {
|
||||
showAlert('Error: ' + data.error, 'error');
|
||||
}
|
||||
})
|
||||
.catch(error => showAlert('Error: ' + error, 'error'));
|
||||
}
|
||||
|
||||
function deleteGreeting(filename, index) {
|
||||
if (confirm(`Delete greeting '${filename}'?`)) {
|
||||
fetch('/delete_greeting/' + filename, { method: 'POST' })
|
||||
|
||||
Reference in New Issue
Block a user