Introduce a styles/ plugin package: - styles/base.py: AlarmStyle ABC (injected set_led, update()->bool contract) - styles/blink.py: BlinkStyle, the existing count-the-blinks puzzle moved out of wecker.py's AlarmClock - styles/simple.py: SimpleStyle, press-once-to-stop (the new default style) - styles/__init__.py: STYLES registry + get_style() validator + LEGACY_STYLE wecker.py now resolves the style via the registry and owns only the shared music/button/cleanup loop; the --style CLI arg defaults to blink so existing cron entries keep their behaviour. Unknown styles raise before hardware init.
149 lines
5.3 KiB
Python
149 lines
5.3 KiB
Python
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.
|
|
"""
|
|
|
|
def __init__(self, set_led):
|
|
super().__init__(set_led)
|
|
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
|
|
|
|
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.
|
|
style = BlinkStyle(lambda on: None)
|
|
|
|
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")
|