Initial commit: Add smart arcade button alarm clock
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
__pycache__
|
||||
*.log
|
||||
@@ -0,0 +1,4 @@
|
||||
# System Context
|
||||
|
||||
This project runs on a **Raspberry Pi 4 Model B (2018)**.
|
||||
Please take this into account for all code changes, dependencies (e.g., ARM architecture), and performance optimizations.
|
||||
@@ -0,0 +1,43 @@
|
||||
# Arcade Button Alarm Clock
|
||||
|
||||
A unique Raspberry Pi-based alarm clock that makes sure you are fully awake before it turns off!
|
||||
|
||||
Instead of simply pressing a button to stop the alarm, this clock requires you to solve a short memory and attention puzzle. When the alarm rings, you press the button, and the built-in LED will blink a random number of times (between 1 and 7). You then have to press the button exactly that many times to confirm. Get it right, and the music stops. Get it wrong, and you'll have to try again!
|
||||
|
||||
## Hardware Requirements
|
||||
* **Raspberry Pi 4 Model B** (2018)
|
||||
* **33mm Illuminated Arcade Button** (e.g., from bastelgarage.ch)
|
||||
* **External Speakers** (connected via 3.5mm jack or a USB Soundcard for better audio quality)
|
||||
|
||||
## Software Requirements
|
||||
This project uses Python 3. The required dependencies are:
|
||||
* `RPi.GPIO` (usually pre-installed on Raspberry Pi OS)
|
||||
* `pygame` (used for optimized audio playback)
|
||||
|
||||
You can install the required packages via:
|
||||
```bash
|
||||
sudo apt-get install python3-pygame python3-rpi.gpio
|
||||
```
|
||||
|
||||
## Wiring
|
||||
Please refer to the [arcade-button-wiring.md](arcade-button-wiring.md) file for detailed instructions on how to connect the arcade button and its LED to the Raspberry Pi's GPIO pins.
|
||||
|
||||
## Usage
|
||||
1. Make sure your audio file (`Laid Back - Sunshine Reggae.mp3` or any other MP3 you prefer) is in the project directory.
|
||||
2. Run the alarm clock script:
|
||||
```bash
|
||||
python3 wecker.py
|
||||
```
|
||||
|
||||
## How the Puzzle Works
|
||||
1. **Ringing:** The music plays in an endless loop.
|
||||
2. **Start:** Press the arcade button once to start the puzzle. Wait 3 seconds.
|
||||
3. **Observe:** The arcade button's LED will blink between 1 and 7 times.
|
||||
4. **Input:** Press the button the exact number of times the LED blinked. Every press is confirmed by the LED lighting up.
|
||||
5. **Wait:** Stop pressing for 3 seconds to lock in your answer.
|
||||
6. **Evaluation:**
|
||||
* *Correct:* The music stops and the script exits.
|
||||
* *Incorrect:* The alarm waits a few seconds and generates a completely new sequence for you to solve.
|
||||
|
||||
## Author
|
||||
Markus Graf (info@marksugraf.ch)
|
||||
@@ -0,0 +1,72 @@
|
||||
# Arcade Button Wiring - Pi 4
|
||||
|
||||
## Components
|
||||
- Arcade Button 33mm illuminated (bastelgarage.ch)
|
||||
- Raspberry Pi 4 Model B
|
||||
|
||||
## GPIO Header Pinout
|
||||
|
||||
```
|
||||
3.3V (1) (2) 5V
|
||||
GPIO 2 (3) (4) 5V
|
||||
GPIO 3 (5) (6) GND
|
||||
GPIO 4 (7) (8) GPIO 14
|
||||
GND (9) (10) GPIO 15
|
||||
GPIO 17 (11) (12) GPIO 18
|
||||
GPIO 27 (13) (14) GND ← LED
|
||||
GND (15) (16) GPIO 23
|
||||
...
|
||||
```
|
||||
|
||||
## Circuit (without transistor)
|
||||
|
||||
### LED (GPIO controlled)
|
||||
```
|
||||
3.3V (Pin 1) ─── LED + (from button)
|
||||
│
|
||||
LED -
|
||||
│
|
||||
GPIO 27 (Pin 13) ───┘
|
||||
```
|
||||
|
||||
### Button
|
||||
```
|
||||
GPIO 17 (Pin 11) ─── Button Contact 1
|
||||
Button Contact 2 ─── GND (Pin 6)
|
||||
```
|
||||
|
||||
## Logic
|
||||
| GPIO 27 | LED |
|
||||
|---------|------|
|
||||
| HIGH | OFF |
|
||||
| LOW | ON |
|
||||
|
||||
## LED Specs (resistor already included)
|
||||
- Operating voltage: 5V to 12V
|
||||
- At 3.3V: slightly less bright, but works
|
||||
|
||||
## Next Steps
|
||||
1. Verify wiring
|
||||
2. Python script for testing:
|
||||
```python
|
||||
import RPi.GPIO as GPIO
|
||||
import time
|
||||
|
||||
GPIO.setmode(GPIO.BCM)
|
||||
|
||||
# LED on GPIO 27
|
||||
GPIO.setup(27, GPIO.OUT)
|
||||
|
||||
# Button on GPIO 17
|
||||
GPIO.setup(17, GPIO.IN, pull_up_down=GPIO.PUD_UP)
|
||||
|
||||
try:
|
||||
while True:
|
||||
if GPIO.input(17) == False: # Button pressed
|
||||
GPIO.output(27, GPIO.LOW) # LED ON
|
||||
else:
|
||||
GPIO.output(27, GPIO.HIGH) # LED OFF
|
||||
time.sleep(0.1)
|
||||
except KeyboardInterrupt:
|
||||
GPIO.cleanup()
|
||||
```
|
||||
@@ -0,0 +1,152 @@
|
||||
import RPi.GPIO as GPIO
|
||||
import time
|
||||
import logging
|
||||
import pygame
|
||||
import random
|
||||
import sys
|
||||
|
||||
# Configure logging
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(message)s',
|
||||
handlers=[
|
||||
logging.FileHandler("wecker.log"),
|
||||
logging.StreamHandler()
|
||||
]
|
||||
)
|
||||
|
||||
# GPIO Setup
|
||||
BUTTON_PIN = 17
|
||||
LED_PIN = 27
|
||||
|
||||
GPIO.setmode(GPIO.BCM)
|
||||
GPIO.setup(LED_PIN, GPIO.OUT)
|
||||
GPIO.setup(BUTTON_PIN, GPIO.IN, pull_up_down=GPIO.PUD_UP)
|
||||
|
||||
# Audio Setup (with optimized buffer)
|
||||
pygame.mixer.pre_init(frequency=44100, size=-16, channels=2, buffer=4096)
|
||||
pygame.mixer.init()
|
||||
|
||||
try:
|
||||
pygame.mixer.music.load("Laid Back - Sunshine Reggae.mp3")
|
||||
pygame.mixer.music.set_volume(1.0)
|
||||
except Exception as e:
|
||||
logging.error(f"Error loading audio file: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
# State Machine states for the flow
|
||||
STATE_RINGING = 0
|
||||
STATE_WAIT_BEFORE_BLINK = 1
|
||||
STATE_BLINKING = 2
|
||||
STATE_WAIT_FOR_INPUT = 3
|
||||
STATE_EVALUATING = 4
|
||||
STATE_WAIT_BEFORE_RETRY = 5
|
||||
|
||||
state = STATE_RINGING
|
||||
target_blinks = 0
|
||||
user_presses = 0
|
||||
last_interaction_time = 0
|
||||
button_was_pressed = False
|
||||
|
||||
def set_led(on):
|
||||
"""Turns the LED on or off (LOW = ON, HIGH = OFF)"""
|
||||
if on:
|
||||
GPIO.output(LED_PIN, GPIO.LOW)
|
||||
else:
|
||||
GPIO.output(LED_PIN, GPIO.HIGH)
|
||||
|
||||
def blink_led(times):
|
||||
"""Blinks the LED a specific number of times (blocking)"""
|
||||
for _ in range(times):
|
||||
set_led(True)
|
||||
time.sleep(0.3) # LED on for 300ms
|
||||
set_led(False)
|
||||
time.sleep(0.3) # LED off for 300ms
|
||||
|
||||
logging.info("Alarm clock started. Music is playing in an endless loop.")
|
||||
# -1 means the song is played in an endless loop
|
||||
pygame.mixer.music.play(-1)
|
||||
|
||||
set_led(False) # LED off at start
|
||||
|
||||
try:
|
||||
while True:
|
||||
now = time.time()
|
||||
|
||||
# Read button state (LOW = pressed, due to Pull-Up)
|
||||
is_pressed = (GPIO.input(BUTTON_PIN) == GPIO.LOW)
|
||||
|
||||
button_just_pressed = False
|
||||
if is_pressed and not button_was_pressed:
|
||||
button_was_pressed = True
|
||||
button_just_pressed = True
|
||||
elif not is_pressed and button_was_pressed:
|
||||
button_was_pressed = False
|
||||
|
||||
# --- State Machine ---
|
||||
|
||||
if state == STATE_RINGING:
|
||||
if button_just_pressed:
|
||||
logging.info("Alarm button pressed! Puzzle started. Waiting 3 seconds...")
|
||||
state = STATE_WAIT_BEFORE_BLINK
|
||||
last_interaction_time = now
|
||||
|
||||
elif state == STATE_WAIT_BEFORE_BLINK:
|
||||
if now - last_interaction_time >= 3.0:
|
||||
target_blinks = random.randint(1, 7)
|
||||
logging.info(f"Blinking {target_blinks} times...")
|
||||
state = STATE_BLINKING
|
||||
|
||||
elif state == STATE_BLINKING:
|
||||
blink_led(target_blinks)
|
||||
logging.info("Blinking finished. Waiting for input...")
|
||||
state = STATE_WAIT_FOR_INPUT
|
||||
user_presses = 0
|
||||
last_interaction_time = time.time()
|
||||
# Update the button state after the blocking blink
|
||||
button_was_pressed = (GPIO.input(BUTTON_PIN) == GPIO.LOW)
|
||||
|
||||
elif state == STATE_WAIT_FOR_INPUT:
|
||||
# LED lights up as confirmation when the button is pressed
|
||||
set_led(is_pressed)
|
||||
|
||||
if button_just_pressed:
|
||||
user_presses += 1
|
||||
logging.info(f"Button pressed: {user_presses} times")
|
||||
|
||||
if is_pressed:
|
||||
# As long as the button is held down, we keep resetting the timeout
|
||||
last_interaction_time = now
|
||||
|
||||
# If the button was released and 3 seconds have passed without action -> Evaluation
|
||||
if not is_pressed and (now - last_interaction_time >= 3.0):
|
||||
state = STATE_EVALUATING
|
||||
|
||||
elif state == STATE_EVALUATING:
|
||||
logging.info(f"Evaluation: Target={target_blinks}, Entered={user_presses}")
|
||||
if user_presses == target_blinks:
|
||||
logging.info("Puzzle solved correctly! Alarm clock is stopping.")
|
||||
pygame.mixer.music.stop()
|
||||
set_led(False)
|
||||
break # Exits the while loop and thus the program
|
||||
else:
|
||||
logging.info("Incorrect input! Waiting 3 seconds before retrying...")
|
||||
state = STATE_WAIT_BEFORE_RETRY
|
||||
last_interaction_time = time.time()
|
||||
set_led(False)
|
||||
|
||||
elif state == STATE_WAIT_BEFORE_RETRY:
|
||||
if now - last_interaction_time >= 5.0:
|
||||
# Puzzle is repeated with a new random blink count
|
||||
target_blinks = random.randint(1, 7)
|
||||
logging.info(f"New attempt! Blinking {target_blinks} times...")
|
||||
state = STATE_BLINKING
|
||||
|
||||
time.sleep(0.02) # Short polling interval to save CPU and ensure good responsiveness (20ms)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
logging.info("Manually aborted (CTRL+C).")
|
||||
finally:
|
||||
pygame.mixer.quit()
|
||||
GPIO.cleanup()
|
||||
logging.info("Program finished.")
|
||||
Reference in New Issue
Block a user