From 2dde7d8e431b9e8155763296e965705bbca0be66 Mon Sep 17 00:00:00 2001 From: grabowski Date: Fri, 24 Oct 2025 14:57:31 +0700 Subject: [PATCH] Add extra GPIO button support with custom sound playback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- README.md | 23 +++++++-- config.example.json | 6 ++- rotary_phone_web.py | 113 +++++++++++++++++++++++++++++++++++++++++--- 3 files changed, 132 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 7eda059..37ccbc1 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ A Raspberry Pi-based rotary phone system for weddings and events. Guests can pic - **Volume Control**: Adjust playback volume with real-time slider (0-100%) - **Multiple Message Support**: Upload and manage multiple greeting messages - **Active Message Selector**: Choose which greeting plays when the phone is picked up +- **Extra Button Support**: Optional GPIO button to play custom sounds on demand - **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 @@ -100,8 +101,11 @@ nano config.json # or use your preferred editor ```json { "gpio": { - "hook_pin": 17, // GPIO pin for hookswitch - "hook_pressed_state": "LOW" // "LOW" or "HIGH" + "hook_pin": 17, // GPIO pin for hookswitch + "hook_pressed_state": "LOW", // "LOW" or "HIGH" + "extra_button_enabled": true, // Enable extra button feature + "extra_button_pin": 27, // GPIO pin for extra button + "extra_button_pressed_state": "LOW" // "LOW" or "HIGH" }, "audio": { "device_index": 1, // HiFiBerry device index @@ -164,6 +168,7 @@ The web interface provides four main sections: - **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 +- **Set Button**: Click "🔘 Set Button" to assign sound to extra button (if enabled) - **Delete**: Remove unwanted greetings (cannot delete the active one) - **Default Tone**: Generate a classic telephone dial tone @@ -181,6 +186,14 @@ The web interface provides four main sections: 4. **Guest hangs up**: Recording stops and saves automatically 5. **Ready for next call**: System returns to waiting state +### Extra Button Operation (Optional) + +If enabled in `config.json`: +1. **Guest presses button**: System detects GPIO signal +2. **Sound plays**: Configured button sound plays through speaker +3. **Non-blocking**: Button can be pressed anytime, even during recording +4. **Debounced**: 0.5s delay prevents accidental double-presses + ## File Structure ``` @@ -218,7 +231,10 @@ The `config.json` file contains all system settings: { "gpio": { "hook_pin": 17, // GPIO pin number for hookswitch - "hook_pressed_state": "LOW" // "LOW" or "HIGH" depending on switch + "hook_pressed_state": "LOW", // "LOW" or "HIGH" depending on switch + "extra_button_enabled": true, // Enable optional extra button + "extra_button_pin": 27, // GPIO pin for extra button + "extra_button_pressed_state": "LOW" // Button pressed state }, "audio": { "device_index": 1, // Audio device index (run test to find) @@ -239,6 +255,7 @@ The `config.json` file contains all system settings: }, "system": { "active_greeting": "dialtone.wav", // Default greeting + "extra_button_sound": "button_sound.wav", // Default button sound "volume": 70 // Default volume (0-100) } } diff --git a/config.example.json b/config.example.json index fb8a003..0a65dfc 100644 --- a/config.example.json +++ b/config.example.json @@ -1,7 +1,10 @@ { "gpio": { "hook_pin": 17, - "hook_pressed_state": "LOW" + "hook_pressed_state": "LOW", + "extra_button_enabled": true, + "extra_button_pin": 27, + "extra_button_pressed_state": "LOW" }, "audio": { "device_index": 1, @@ -22,6 +25,7 @@ }, "system": { "active_greeting": "dialtone.wav", + "extra_button_sound": "button_sound.wav", "volume": 70 } } diff --git a/rotary_phone_web.py b/rotary_phone_web.py index 23912ea..27751c8 100644 --- a/rotary_phone_web.py +++ b/rotary_phone_web.py @@ -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/', 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__":
â„šī¸ Active greeting: {{ active_greeting }} + {% if extra_button_enabled %} +
🔘 Extra button sound: {{ extra_button_sound }} + {% endif %}
@@ -1032,7 +1101,9 @@ if __name__ == "__main__":

- {% if greeting.is_active %}⭐{% endif %} {{ greeting.filename }} + {% if greeting.is_active %}⭐{% endif %} + {% if greeting.is_button_sound and extra_button_enabled %}🔘{% endif %} + {{ greeting.filename }}

📅 {{ greeting.date }} | @@ -1048,6 +1119,11 @@ if __name__ == "__main__": + {% if extra_button_enabled and not greeting.is_button_sound %} + + {% endif %} @@ -1055,6 +1131,11 @@ if __name__ == "__main__": + {% if extra_button_enabled and not greeting.is_button_sound %} + + {% endif %} {% endif %}
@@ -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' })