From 9e888d9afae1ec3855773ebfdbedb2bd21243590 Mon Sep 17 00:00:00 2001 From: Gabby Morgan Date: Fri, 5 Jun 2026 01:53:25 -0500 Subject: [PATCH] refactor gesture handling to use timers and callbacks instead of loops --- bridge_whitelist.json | 2 +- client.py | 107 ++++++++++++++++++-------------- whisplay_interface.py | 139 ++++++++++++++++++------------------------ 3 files changed, 123 insertions(+), 125 deletions(-) diff --git a/bridge_whitelist.json b/bridge_whitelist.json index 716ef99..e53ffe5 100644 --- a/bridge_whitelist.json +++ b/bridge_whitelist.json @@ -1,3 +1,3 @@ [ - "8567367936f085f93bf4ecdf494fafd0" + "8567367936f085f93bf4ecdf494fafd0", "c64bc5a948096bd624bd9c186c35bdb1" ] diff --git a/client.py b/client.py index 5eccd1e..00fdfe9 100644 --- a/client.py +++ b/client.py @@ -50,7 +50,6 @@ class ChannelClient: self._ptt_enabled = True self._ptt_active = False self.whisplay_interface = None - self._gesture_thread = None self._init_whisplay() @@ -125,6 +124,12 @@ class ChannelClient: self.whisplay_interface.board.on_focus_revoked(self._on_focus_revoked) self._load_fonts() self._setup_whisplay_audio() + self.whisplay_interface.on_press(self._on_press) + self.whisplay_interface.on_release(self._on_release) + self.whisplay_interface.on_long_press(self._on_long_press) + self.whisplay_interface.on_long_release(self._on_long_release) + self.whisplay_interface.on_double_press(self._on_double_press) + self.whisplay_interface.on_double_release(self._on_double_release) def _find_wm8960_card(self): try: @@ -286,52 +291,66 @@ class ChannelClient: fb = self._render(title, lines, accent=accent) self.whisplay_interface.board.draw_image(0, 0, self.whisplay_interface.board.LCD_WIDTH, self.whisplay_interface.board.LCD_HEIGHT, fb) - def _gesture_loop(self): - while self.should_run: - self.whisplay_interface.update_gestures() + def _on_press(self): + if self.state == self.STATE_IN_CALL: + if self._ptt_enabled and not self._ptt_active: + self._ptt_active = True + self._hw_set_mic_muted(False) + if self.whisplay_interface.board: + self.whisplay_interface.board.set_rgb(255, 0, 0) - # Double-press (highest priority) - if self.whisplay_interface.was_double_pressed and self.state == self.STATE_IN_CALL: - self.telephone.hangup() - time.sleep(0.02) - continue + def _on_double_press(self): + if self.state == self.STATE_READY: + self._on_press() - # STATE_READY: release actions - if self.state == self.STATE_READY and self.whisplay_interface.was_pressed: - if self.whisplay_interface.was_long_pressed and self.channels: - self.whisplay_interface.board.set_rgb(255, 180, 0) - self.call_channel(self.selected_index) - elif self.channels: - self.selected_index = (self.selected_index + 1) % len(self.channels) - self._show_channels() + def _on_long_press(self): + pass - # STATE_IN_CALL: hold-to-talk + release actions - if self.state == self.STATE_IN_CALL: - if self.whisplay_interface.is_pressed and self._ptt_enabled and not self._ptt_active: - self._ptt_active = True - self._hw_set_mic_muted(False) - if self.whisplay_interface.board: - self.whisplay_interface.board.set_rgb(255, 255, 0) + def _on_release(self): + if self.state == self.STATE_READY: + if self.channels: + self.selected_index = (self.selected_index + 1) % len(self.channels) + self._show_channels() - if self.whisplay_interface.was_pressed: - if self._ptt_enabled: - if not self.whisplay_interface.was_long_pressed: - self._ptt_active = False - self._ptt_enabled = False - if self.whisplay_interface.board: - self._show_in_call() - else: - self._ptt_active = False - self._hw_set_mic_muted(True) - if self.whisplay_interface.board: - self.whisplay_interface.board.set_rgb(0, 255, 80) - elif not self.whisplay_interface.was_long_pressed: - self._ptt_enabled = True - self._hw_set_mic_muted(True) - if self.whisplay_interface.board: - self._show_in_call() + elif self.state == self.STATE_IN_CALL: + if self._ptt_enabled: + self._ptt_enabled = False + self._ptt_active = False + if self.whisplay_interface.board: + self.whisplay_interface.board.set_rgb(255, 0, 0) - time.sleep(0.02) + else: + self._ptt_enabled = True + self._ptt_active = False + self._hw_set_mic_muted(True) + if self.whisplay_interface.board: + self.whisplay_interface.board.set_rgb(0, 0, 0) + + if self.whisplay_interface.board: + self._show_in_call() + + def _on_double_release(self): + if self.state == self.STATE_IN_CALL: + self.telephone.hangup() + self.selected_index = (self.selected_index + 1) % len(self.channels) + self._show_channels() + + def _on_long_release(self): + if self.state == self.STATE_IN_CALL and self._ptt_enabled: + self._ptt_active = False + self._hw_set_mic_muted(True) + if self.whisplay_interface.board: + self.whisplay_interface.board.set_rgb(0, 0, 0) + elif self.state == self.STATE_READY: + if self.channels: + if self._ptt_enabled: + self.whisplay_interface.board.set_rgb(0, 0, 0) + else: + self.whisplay_interface.board.set_rgb(255, 0, 0) + self.call_channel(self.selected_index) + + else: + self._on_release() def _on_exit_request(self, payload=None): self.cleanup() @@ -572,8 +591,6 @@ class ChannelClient: self.selected_index = 0 self.whisplay_interface.board.set_rgb(0, 80, 255) self._show_channels() - self._gesture_thread = threading.Thread(target=self._gesture_loop, daemon=True) - self._gesture_thread.start() while self.should_run: if self.state == self.STATE_CALLING: self._show_status("Calling...", f"Channel {self.selected_index + 1}", @@ -586,8 +603,6 @@ class ChannelClient: if self.telephone and self.state == self.STATE_IN_CALL: self.telephone.hangup() self.should_run = False - if self._gesture_thread: - self._gesture_thread.join(timeout=1) if self.whisplay_interface.board: self.whisplay_interface.board.cleanup() diff --git a/whisplay_interface.py b/whisplay_interface.py index 389867d..fd15ea0 100644 --- a/whisplay_interface.py +++ b/whisplay_interface.py @@ -12,50 +12,20 @@ class WhisplayInterface: def __init__(self): self.board = None self._lock = threading.Lock() + self._is_pressing = False + self._is_double_pressing = False + self._is_long_pressing = False + self._long_press_timer_thread = None self._button_press_start = None self._button_press_end = None - self._button_is_pressed = False - self._button_was_pressed = False - self._button_is_long_pressed = False - self._button_was_long_pressed = False - self._button_was_double_pressed = False + self._on_press_callback = None + self._on_release_callback = None + self._on_long_press_callback = None + self._on_long_release_callback = None + self._on_double_press_callback = None + self._on_double_release_callback = None - # Edge-detection state - self._prev_is_pressed = False - self._prev_press_end = None - - self.set_wm8960_volume_stable(speaker=250, mic=30) - - # -- Properties -- - - @property - def is_pressed(self): - return self._button_is_pressed - - @property - def was_pressed(self): - return self._button_was_pressed - - @property - def is_long_pressed(self): - return self._button_is_long_pressed - - @property - def was_long_pressed(self): - return self._button_was_long_pressed - - @property - def was_double_pressed(self): - return self._button_was_double_pressed - - @property - def press_duration(self): - if self._button_press_start is None: - return 0.0 - end = self._button_press_end if self._button_press_end is not None else time.time() - return end - self._button_press_start - - # -- Attachment -- + self.set_wm8960_volume_stable(speaker=200, mic=80) def create_board(self): self.board = runtime.whisplay_client.create_whisplay_hardware( @@ -70,53 +40,66 @@ class WhisplayInterface: # -- Internal press/release (called from GPIO thread) -- + def on_press(self, callback): + self._on_press_callback = callback + + def on_release(self, callback): + self._on_release_callback = callback + + def on_long_press(self, callback): + self._on_long_press_callback = callback + + def on_long_release(self, callback): + self._on_long_release_callback = callback + + def on_double_press(self, callback): + self._on_double_press_callback = callback + + def on_double_release(self, callback): + self._on_double_release_callback = callback + + def _on_button_press(self): with self._lock: - self._button_is_pressed = True + self._is_pressing = True self._button_press_start = time.time() + if self._button_press_end is not None: + time_since_last_press = self._button_press_start- self._button_press_end + if time_since_last_press <= self.DOUBLE_PRESS_THRESHOLD: + self._is_double_pressing = True + if self._on_double_press_callback: + self._on_double_press_callback() + if not self._is_double_pressing: + if self._on_press_callback: + self._on_press_callback() + self._long_press_timer_thread = threading.Timer(self.LONG_PRESS_THRESHOLD, self._long_press_timer) + self._long_press_timer_thread.start() + def _on_button_release(self): with self._lock: - self._button_is_pressed = False + self._long_press_timer_thread.cancel() + self._is_pressing = False self._button_press_end = time.time() + if self._is_double_pressing: + if self._on_double_release_callback: + self._on_double_release_callback() + self._is_double_pressing = False + elif self._is_long_pressing: + if self._on_long_release_callback: + self._on_long_release_callback() + self._is_long_pressing = False + else: + if self._on_release_callback: + self._on_release_callback() - # -- Polled update (called from gesture thread) -- - - def update_gestures(self): - self._button_was_pressed = False - self._button_was_long_pressed = False - self._button_was_double_pressed = False - + def _long_press_timer(self): with self._lock: - is_pressed = self._button_is_pressed - press_start = self._button_press_start - press_end = self._button_press_end + if self._is_pressing: + self._is_long_pressing = True + if self._on_long_press_callback: + self._on_long_press_callback() - now = time.time() - - # Press edge: transitioned from not-pressed to pressed - if not self._prev_is_pressed and is_pressed: - if self._prev_press_end is not None and press_start is not None: - gap = press_start - self._prev_press_end - if gap < WhisplayInterface.DOUBLE_PRESS_THRESHOLD: - self._button_was_double_pressed = True - - # Release edge: transitioned from pressed to not-pressed - if self._prev_is_pressed and not is_pressed: - self._button_was_pressed = True - if press_start is not None and press_end is not None: - duration = press_end - press_start - if duration >= WhisplayInterface.LONG_PRESS_THRESHOLD: - self._button_was_long_pressed = True - self._prev_press_end = press_end - - # Level flag: currently held past threshold - if is_pressed and press_start is not None: - self._button_is_long_pressed = (now - press_start) >= WhisplayInterface.LONG_PRESS_THRESHOLD - else: - self._button_is_long_pressed = False - - self._prev_is_pressed = is_pressed def set_wm8960_volume_stable(self, speaker: int = 0, mic: int = 0): CARD_NAME = 'wm8960soundcard'