abstract gesture handling to it's own module
This commit is contained in:
@@ -33,16 +33,17 @@ Single-file Python application for managing LXST Channel connections via Reticul
|
|||||||
Short press (<1s) Cycle to next channel in list
|
Short press (<1s) Cycle to next channel in list
|
||||||
Long press (>=1s) Call the selected channel
|
Long press (>=1s) Call the selected channel
|
||||||
|
|
||||||
### In call (STATE_IN_CALL) — Push-to-Talk mode (default)
|
### In call — Push-to-Talk mode (default)
|
||||||
|
|
||||||
Press & hold Talk (unmute microphone)
|
Hold (>=1s) Talk (unmute microphone)
|
||||||
Release Stop talking (mute microphone)
|
Release Stop talking (mute microphone)
|
||||||
Double-press Toggle to always-on mode
|
Short press (<1s) Switch to always-on mode
|
||||||
|
Double press Hang up
|
||||||
|
|
||||||
### In call — Always-on mode (toggled)
|
### In call — Always-on mode (toggled)
|
||||||
|
|
||||||
Press Hang up
|
Short press (<1s) Switch to push-to-talk mode
|
||||||
Double-press Toggle to push-to-talk mode
|
Double press Hang up
|
||||||
|
|
||||||
### RGB LED indicates state:
|
### RGB LED indicates state:
|
||||||
Blue = ready, showing channels
|
Blue = ready, showing channels
|
||||||
|
|||||||
@@ -35,8 +35,7 @@ if _WHISPLAY_AVAILABLE:
|
|||||||
except ImportError:
|
except ImportError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
LONG_PRESS_THRESHOLD = 1.0
|
|
||||||
DOUBLE_PRESS_THRESHOLD = 0.4
|
|
||||||
|
|
||||||
|
|
||||||
class Terminal:
|
class Terminal:
|
||||||
@@ -70,14 +69,11 @@ class ChannelClient:
|
|||||||
# ── Whisplay HAT init ──
|
# ── Whisplay HAT init ──
|
||||||
self.board = None
|
self.board = None
|
||||||
self.selected_index = 0
|
self.selected_index = 0
|
||||||
self._press_time = 0.0
|
|
||||||
self._wm8960_device = None
|
self._wm8960_device = None
|
||||||
self._ptt_enabled = True
|
self._ptt_enabled = True
|
||||||
self._ptt_active = False
|
self._ptt_active = False
|
||||||
self._last_release_time = 0.0
|
self.gestures = None
|
||||||
self._double_pressed = False
|
|
||||||
self._press_count = 0
|
|
||||||
self._triple_pressed = False
|
|
||||||
if use_whisplay and _WHISPLAY_AVAILABLE and _HAS_PIL:
|
if use_whisplay and _WHISPLAY_AVAILABLE and _HAS_PIL:
|
||||||
try:
|
try:
|
||||||
self._init_whisplay()
|
self._init_whisplay()
|
||||||
@@ -156,8 +152,10 @@ class ChannelClient:
|
|||||||
use_daemon_default_log=True,
|
use_daemon_default_log=True,
|
||||||
)
|
)
|
||||||
self.board.set_backlight(70)
|
self.board.set_backlight(70)
|
||||||
self.board.on_button_press(self._on_button_press)
|
from whisplay_interface import WhisplayInterface
|
||||||
self.board.on_button_release(self._on_button_release)
|
self.gestures = WhisplayInterface(self.board)
|
||||||
|
self.gestures.on_press(self._on_button_press)
|
||||||
|
self.gestures.on_release(self._on_button_release)
|
||||||
if hasattr(self.board, "on_exit_request"):
|
if hasattr(self.board, "on_exit_request"):
|
||||||
self.board.on_exit_request(self._on_exit_request)
|
self.board.on_exit_request(self._on_exit_request)
|
||||||
if hasattr(self.board, "on_focus_revoked"):
|
if hasattr(self.board, "on_focus_revoked"):
|
||||||
@@ -312,11 +310,11 @@ class ChannelClient:
|
|||||||
if ch:
|
if ch:
|
||||||
lines.append(ch["hash"][:16] + "..")
|
lines.append(ch["hash"][:16] + "..")
|
||||||
if self._ptt_enabled:
|
if self._ptt_enabled:
|
||||||
lines += ["", "PTT: hold to talk", "Release to mute"]
|
lines += ["", "PTT: hold to talk", "Short: always-on"]
|
||||||
footer = "Dbl: toggle Trip: leave"
|
footer = "Dbl: leave"
|
||||||
else:
|
else:
|
||||||
lines += ["", "Mic always-on", "Press to hang up"]
|
lines += ["", "Always-on (unmuted)"]
|
||||||
footer = "Dbl: toggle Trip: leave"
|
footer = "Short: PTT Dbl: leave"
|
||||||
fb = self._render("In Call", lines, accent=(60, 220, 120), footer=footer)
|
fb = self._render("In Call", lines, accent=(60, 220, 120), footer=footer)
|
||||||
self.board.draw_image(0, 0, self.board.LCD_WIDTH, self.board.LCD_HEIGHT, fb)
|
self.board.draw_image(0, 0, self.board.LCD_WIDTH, self.board.LCD_HEIGHT, fb)
|
||||||
|
|
||||||
@@ -328,33 +326,10 @@ class ChannelClient:
|
|||||||
self.board.draw_image(0, 0, self.board.LCD_WIDTH, self.board.LCD_HEIGHT, fb)
|
self.board.draw_image(0, 0, self.board.LCD_WIDTH, self.board.LCD_HEIGHT, fb)
|
||||||
|
|
||||||
def _on_button_press(self):
|
def _on_button_press(self):
|
||||||
self._press_time = time.time()
|
|
||||||
if self.state == self.STATE_IN_CALL:
|
if self.state == self.STATE_IN_CALL:
|
||||||
gap = time.time() - self._last_release_time
|
if self.gestures.is_double_pressed:
|
||||||
if gap < DOUBLE_PRESS_THRESHOLD:
|
|
||||||
self._press_count += 1
|
|
||||||
else:
|
|
||||||
self._press_count = 1
|
|
||||||
|
|
||||||
if self._press_count >= 3:
|
|
||||||
self._press_count = 0
|
|
||||||
self._triple_pressed = True
|
|
||||||
self._double_pressed = False
|
|
||||||
self._ptt_active = False
|
|
||||||
self.telephone.hangup()
|
self.telephone.hangup()
|
||||||
return
|
return
|
||||||
|
|
||||||
if self._press_count == 2:
|
|
||||||
self._double_pressed = True
|
|
||||||
self._ptt_enabled = not self._ptt_enabled
|
|
||||||
if self._ptt_enabled:
|
|
||||||
self.telephone.mute_transmit()
|
|
||||||
else:
|
|
||||||
self.telephone.unmute_transmit()
|
|
||||||
if self.board:
|
|
||||||
self._show_in_call()
|
|
||||||
return
|
|
||||||
|
|
||||||
if self._ptt_enabled:
|
if self._ptt_enabled:
|
||||||
self._ptt_active = True
|
self._ptt_active = True
|
||||||
self.telephone.unmute_transmit()
|
self.telephone.unmute_transmit()
|
||||||
@@ -362,27 +337,33 @@ class ChannelClient:
|
|||||||
self.board.set_rgb(255, 255, 0)
|
self.board.set_rgb(255, 255, 0)
|
||||||
|
|
||||||
def _on_button_release(self):
|
def _on_button_release(self):
|
||||||
self._last_release_time = time.time()
|
if self.gestures.was_double_pressed:
|
||||||
if self._triple_pressed:
|
self.gestures.acknowledge_double_press()
|
||||||
self._triple_pressed = False
|
|
||||||
return
|
return
|
||||||
if self.state == self.STATE_IN_CALL:
|
if self.state == self.STATE_IN_CALL:
|
||||||
if self._double_pressed:
|
elapsed = self.gestures.press_duration
|
||||||
self._double_pressed = False
|
|
||||||
return
|
|
||||||
if self._ptt_enabled:
|
if self._ptt_enabled:
|
||||||
|
if elapsed < self.gestures.long_press_threshold:
|
||||||
|
self._ptt_active = False
|
||||||
|
self._ptt_enabled = False
|
||||||
|
if self.board:
|
||||||
|
self._show_in_call()
|
||||||
|
else:
|
||||||
self._ptt_active = False
|
self._ptt_active = False
|
||||||
self.telephone.mute_transmit()
|
self.telephone.mute_transmit()
|
||||||
if self.board:
|
if self.board:
|
||||||
self.board.set_rgb(0, 255, 80)
|
self.board.set_rgb(0, 255, 80)
|
||||||
return
|
|
||||||
else:
|
else:
|
||||||
self.telephone.hangup()
|
if elapsed < self.gestures.long_press_threshold:
|
||||||
|
self._ptt_enabled = True
|
||||||
|
self.telephone.mute_transmit()
|
||||||
|
if self.board:
|
||||||
|
self._show_in_call()
|
||||||
return
|
return
|
||||||
if self.state != self.STATE_READY:
|
if self.state != self.STATE_READY:
|
||||||
return
|
return
|
||||||
elapsed = time.time() - self._press_time
|
elapsed = self.gestures.press_duration
|
||||||
if elapsed >= LONG_PRESS_THRESHOLD:
|
if elapsed >= self.gestures.long_press_threshold:
|
||||||
if self.channels:
|
if self.channels:
|
||||||
self.board.set_rgb(255, 180, 0)
|
self.board.set_rgb(255, 180, 0)
|
||||||
self.call_channel(self.selected_index)
|
self.call_channel(self.selected_index)
|
||||||
@@ -390,6 +371,7 @@ class ChannelClient:
|
|||||||
if self.channels:
|
if self.channels:
|
||||||
self.selected_index = (self.selected_index + 1) % len(self.channels)
|
self.selected_index = (self.selected_index + 1) % len(self.channels)
|
||||||
self._show_channels()
|
self._show_channels()
|
||||||
|
self.gestures.acknowledge_press()
|
||||||
|
|
||||||
def _on_exit_request(self, payload=None):
|
def _on_exit_request(self, payload=None):
|
||||||
self.cleanup()
|
self.cleanup()
|
||||||
@@ -525,9 +507,8 @@ class ChannelClient:
|
|||||||
was_calling = self.state == self.STATE_CALLING
|
was_calling = self.state == self.STATE_CALLING
|
||||||
self.state = self.STATE_READY
|
self.state = self.STATE_READY
|
||||||
self._ptt_active = False
|
self._ptt_active = False
|
||||||
self._double_pressed = False
|
if self.gestures:
|
||||||
self._triple_pressed = False
|
self.gestures.acknowledge_double_press()
|
||||||
self._press_count = 0
|
|
||||||
if was_in_call:
|
if was_in_call:
|
||||||
print(f"\nCall ended")
|
print(f"\nCall ended")
|
||||||
elif was_calling:
|
elif was_calling:
|
||||||
@@ -698,6 +679,8 @@ class ChannelClient:
|
|||||||
if self.telephone and self.state == self.STATE_IN_CALL:
|
if self.telephone and self.state == self.STATE_IN_CALL:
|
||||||
self.telephone.hangup()
|
self.telephone.hangup()
|
||||||
self.should_run = False
|
self.should_run = False
|
||||||
|
if self.gestures:
|
||||||
|
self.gestures.stop()
|
||||||
if self.board:
|
if self.board:
|
||||||
self.board.cleanup()
|
self.board.cleanup()
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,140 @@
|
|||||||
|
import time
|
||||||
|
import threading
|
||||||
|
|
||||||
|
|
||||||
|
class WhisplayInterface:
|
||||||
|
def __init__(self, board=None, long_press_threshold=0.5, double_press_threshold=0.3):
|
||||||
|
self._board = board
|
||||||
|
self._long_press_threshold = long_press_threshold
|
||||||
|
self._double_press_threshold = double_press_threshold
|
||||||
|
self._button_is_double_pressed_set = False
|
||||||
|
|
||||||
|
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_is_double_pressed = False
|
||||||
|
self._button_was_double_pressed = False
|
||||||
|
|
||||||
|
self._press_callbacks = []
|
||||||
|
self._release_callbacks = []
|
||||||
|
self._running = False
|
||||||
|
self._gesture_thread = None
|
||||||
|
|
||||||
|
if board is not None:
|
||||||
|
self.attach(board)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def long_press_threshold(self):
|
||||||
|
return self._long_press_threshold
|
||||||
|
|
||||||
|
@property
|
||||||
|
def double_press_threshold(self):
|
||||||
|
return self._double_press_threshold
|
||||||
|
|
||||||
|
@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 is_double_pressed(self):
|
||||||
|
return self._button_is_double_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
|
||||||
|
|
||||||
|
def attach(self, board):
|
||||||
|
self._board = board
|
||||||
|
self._board.on_button_press(self._on_button_press)
|
||||||
|
self._board.on_button_release(self._on_button_release)
|
||||||
|
self.start()
|
||||||
|
|
||||||
|
def start(self):
|
||||||
|
if self._gesture_thread is not None:
|
||||||
|
return
|
||||||
|
self._running = True
|
||||||
|
self._gesture_thread = threading.Thread(daemon=True, target=self._gesture_loop)
|
||||||
|
self._gesture_thread.start()
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
self._running = False
|
||||||
|
|
||||||
|
def on_press(self, callback):
|
||||||
|
self._press_callbacks.append(callback)
|
||||||
|
|
||||||
|
def on_release(self, callback):
|
||||||
|
self._release_callbacks.append(callback)
|
||||||
|
|
||||||
|
def acknowledge_press(self):
|
||||||
|
self._button_was_pressed = False
|
||||||
|
|
||||||
|
def acknowledge_long_press(self):
|
||||||
|
self._button_was_long_pressed = False
|
||||||
|
|
||||||
|
def acknowledge_double_press(self):
|
||||||
|
self._button_was_double_pressed = False
|
||||||
|
|
||||||
|
def _on_button_press(self):
|
||||||
|
self._button_is_pressed = True
|
||||||
|
self._button_press_start = time.time()
|
||||||
|
if self._button_press_end is not None:
|
||||||
|
gap = self._button_press_start - self._button_press_end
|
||||||
|
if gap < self._double_press_threshold:
|
||||||
|
self._button_is_double_pressed = True
|
||||||
|
self._button_is_double_pressed_set = True
|
||||||
|
for cb in self._press_callbacks:
|
||||||
|
cb()
|
||||||
|
|
||||||
|
def _on_button_release(self):
|
||||||
|
self._button_was_pressed = self._button_is_pressed
|
||||||
|
self._button_press_end = time.time()
|
||||||
|
self._button_was_long_pressed = self._button_is_long_pressed
|
||||||
|
self._button_was_double_pressed = self._button_is_double_pressed
|
||||||
|
self._button_is_pressed = False
|
||||||
|
self._button_is_double_pressed = False
|
||||||
|
self._button_is_long_pressed = False
|
||||||
|
self._button_is_double_pressed_set = False
|
||||||
|
for cb in self._release_callbacks:
|
||||||
|
cb()
|
||||||
|
|
||||||
|
def _gesture_loop(self):
|
||||||
|
while self._running:
|
||||||
|
try:
|
||||||
|
current_time = time.time()
|
||||||
|
if self._button_is_pressed:
|
||||||
|
if not self._button_is_double_pressed_set:
|
||||||
|
self._button_is_double_pressed = (
|
||||||
|
self._button_was_pressed
|
||||||
|
and self._button_press_end is not None
|
||||||
|
and current_time - self._button_press_end < self._double_press_threshold
|
||||||
|
)
|
||||||
|
if self._button_is_double_pressed:
|
||||||
|
self._button_is_double_pressed_set = True
|
||||||
|
self._button_is_long_pressed = (
|
||||||
|
current_time - self._button_press_start > self._long_press_threshold
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
time.sleep(0.05)
|
||||||
Reference in New Issue
Block a user