2026-07-31 16:19:14 +02:00
|
|
|
import logging
|
|
|
|
|
import random
|
|
|
|
|
|
|
|
|
|
from styles.base import AlarmStyle
|
|
|
|
|
|
|
|
|
|
# State Machine states for the puzzle 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
|
|
|
|
|
|
|
|
|
|
BLINK_INTERVAL = 0.3
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class BlinkStyle(AlarmStyle):
|
|
|
|
|
"""The count-the-blinks puzzle.
|
|
|
|
|
|
|
|
|
|
Press once to start, watch the LED blink 1-7 times, then press the button
|
|
|
|
|
exactly that many times. A wrong count starts a fresh sequence.
|
|
|
|
|
"""
|
|
|
|
|
|
2026-08-02 16:29:40 +02:00
|
|
|
def __init__(self, set_led, sound, music_file=None):
|
|
|
|
|
super().__init__(set_led, sound, music_file)
|
2026-07-31 16:19:14 +02:00
|
|
|
self.state = STATE_RINGING
|
|
|
|
|
self.target_blinks = 0
|
|
|
|
|
self.user_presses = 0
|
|
|
|
|
self.last_interaction_time = 0
|
|
|
|
|
self.button_was_pressed = False
|
|
|
|
|
self._blink_phase = 0
|
|
|
|
|
self._blink_phases = 0
|
|
|
|
|
self._blink_next_toggle: float | None = None
|
|
|
|
|
|
2026-08-02 16:29:40 +02:00
|
|
|
def start(self):
|
|
|
|
|
# The puzzle rings with music on an endless loop; the LED stays off
|
|
|
|
|
# until the button is pressed to start the blink sequence.
|
|
|
|
|
self.sound.play_music(self.music_file)
|
|
|
|
|
self.set_led(False)
|
|
|
|
|
|
2026-07-31 16:19:14 +02:00
|
|
|
def update(self, now, is_pressed):
|
|
|
|
|
button_just_pressed = False
|
|
|
|
|
if is_pressed and not self.button_was_pressed:
|
|
|
|
|
self.button_was_pressed = True
|
|
|
|
|
button_just_pressed = True
|
|
|
|
|
elif not is_pressed and self.button_was_pressed:
|
|
|
|
|
self.button_was_pressed = False
|
|
|
|
|
|
|
|
|
|
if self.state == STATE_RINGING:
|
|
|
|
|
if button_just_pressed:
|
|
|
|
|
logging.info(
|
|
|
|
|
"Alarm button pressed! Puzzle started. Waiting 3 seconds..."
|
|
|
|
|
)
|
|
|
|
|
self.state = STATE_WAIT_BEFORE_BLINK
|
|
|
|
|
self.last_interaction_time = now
|
|
|
|
|
|
|
|
|
|
elif self.state == STATE_WAIT_BEFORE_BLINK:
|
|
|
|
|
if now - self.last_interaction_time >= 3.0:
|
|
|
|
|
self.target_blinks = random.randint(1, 7)
|
|
|
|
|
logging.info(f"Blinking {self.target_blinks} times...")
|
|
|
|
|
self.state = STATE_BLINKING
|
|
|
|
|
self._blink_phase = 0
|
|
|
|
|
self._blink_phases = self.target_blinks * 2
|
|
|
|
|
self._blink_next_toggle = now + BLINK_INTERVAL
|
|
|
|
|
self.set_led(True)
|
|
|
|
|
|
|
|
|
|
elif self.state == STATE_BLINKING:
|
|
|
|
|
# Non-blocking blink: toggle the LED at fixed intervals so the
|
|
|
|
|
# runner keeps sampling the button and can be interrupted.
|
|
|
|
|
if self._blink_next_toggle is None:
|
|
|
|
|
# Safety for direct state assignment (e.g. tests).
|
|
|
|
|
self._blink_phase = 0
|
|
|
|
|
self._blink_phases = self.target_blinks * 2
|
|
|
|
|
self._blink_next_toggle = now + BLINK_INTERVAL
|
|
|
|
|
self.set_led(True)
|
|
|
|
|
|
|
|
|
|
while now >= self._blink_next_toggle:
|
|
|
|
|
self._blink_phase += 1
|
|
|
|
|
self._blink_next_toggle += BLINK_INTERVAL
|
|
|
|
|
if self._blink_phase >= self._blink_phases:
|
|
|
|
|
self.set_led(False)
|
|
|
|
|
self._blink_phase = 0
|
|
|
|
|
self._blink_phases = 0
|
|
|
|
|
self._blink_next_toggle = None
|
|
|
|
|
logging.info("Blinking finished. Waiting for input...")
|
|
|
|
|
self.state = STATE_WAIT_FOR_INPUT
|
|
|
|
|
self.user_presses = 0
|
|
|
|
|
self.last_interaction_time = now
|
|
|
|
|
self.button_was_pressed = is_pressed
|
|
|
|
|
break
|
|
|
|
|
else:
|
|
|
|
|
self.set_led(self._blink_phase % 2 == 0)
|
|
|
|
|
|
|
|
|
|
elif self.state == STATE_WAIT_FOR_INPUT:
|
|
|
|
|
self.set_led(is_pressed)
|
|
|
|
|
|
|
|
|
|
if button_just_pressed:
|
|
|
|
|
self.user_presses += 1
|
|
|
|
|
logging.info(f"Button pressed: {self.user_presses} times")
|
|
|
|
|
|
|
|
|
|
if is_pressed:
|
|
|
|
|
self.last_interaction_time = now
|
|
|
|
|
|
|
|
|
|
if not is_pressed and (now - self.last_interaction_time >= 3.0):
|
|
|
|
|
self.state = STATE_EVALUATING
|
|
|
|
|
|
|
|
|
|
elif self.state == STATE_EVALUATING:
|
|
|
|
|
logging.info(
|
|
|
|
|
f"Evaluation: Target={self.target_blinks}, "
|
|
|
|
|
f"Entered={self.user_presses}"
|
|
|
|
|
)
|
|
|
|
|
if self.user_presses == self.target_blinks:
|
|
|
|
|
logging.info("Puzzle solved correctly! Alarm clock is stopping.")
|
|
|
|
|
self.set_led(False)
|
|
|
|
|
return False # Signal the runner to stop.
|
|
|
|
|
else:
|
|
|
|
|
logging.info("Incorrect input! Waiting before retrying...")
|
|
|
|
|
self.state = STATE_WAIT_BEFORE_RETRY
|
|
|
|
|
self.last_interaction_time = now
|
|
|
|
|
self.set_led(False)
|
|
|
|
|
|
|
|
|
|
elif self.state == STATE_WAIT_BEFORE_RETRY:
|
|
|
|
|
if now - self.last_interaction_time >= 5.0:
|
|
|
|
|
self.target_blinks = random.randint(1, 7)
|
|
|
|
|
logging.info(f"New attempt! Blinking {self.target_blinks} times...")
|
|
|
|
|
self.state = STATE_BLINKING
|
|
|
|
|
|
|
|
|
|
return True # Keep running
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
# Self-check: a correct blink sequence stops the alarm.
|
2026-08-02 16:29:40 +02:00
|
|
|
class _StubSound:
|
|
|
|
|
def play_music(self, path):
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
def play_beep(self):
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
style = BlinkStyle(lambda on: None, _StubSound(), "x.mp3")
|
2026-07-31 16:19:14 +02:00
|
|
|
|
|
|
|
|
assert style.update(100.0, True) is True
|
|
|
|
|
assert style.state == STATE_WAIT_BEFORE_BLINK
|
|
|
|
|
|
|
|
|
|
import random as _random
|
|
|
|
|
|
|
|
|
|
_random.randint = lambda a, b: 3 # force 3 blinks
|
|
|
|
|
style.update(103.1, False)
|
|
|
|
|
assert style.state == STATE_BLINKING and style.target_blinks == 3
|
|
|
|
|
|
|
|
|
|
style.update(105.0, False)
|
|
|
|
|
assert style.state == STATE_WAIT_FOR_INPUT
|
|
|
|
|
|
|
|
|
|
for t in (105.5, 106.0, 106.5):
|
|
|
|
|
style.update(t, True)
|
|
|
|
|
style.update(t + 0.05, False)
|
|
|
|
|
assert style.user_presses == 3
|
|
|
|
|
|
|
|
|
|
style.update(109.7, False)
|
|
|
|
|
assert style.update(109.8, False) is False # solved -> stop
|
|
|
|
|
print("blink self-check ok")
|