Compare commits
10 Commits
3415b6bb59
..
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 9e888d9afa | |||
| 884325d1c6 | |||
| 3b5bac5d2c | |||
| 63422c9a75 | |||
| 561c014c33 | |||
| b4cd86ed78 | |||
| feed1145f8 | |||
| 5d4e7aa313 | |||
| 183c055f83 | |||
| c5f3cf54f5 |
@@ -1,8 +1,8 @@
|
||||
# LXST Client
|
||||
|
||||
Single-file Python application for managing LXST Channel connections via Reticulum Network (RNS) and LXMF protocols. Supports CLI and Whisplay HAT GUI modes.
|
||||
Single-file Python application for managing LXST Channel connections via Reticulum Network (RNS) and LXMF protocols using the Whisplay HAT.
|
||||
|
||||
## Running (CLI mode, default)
|
||||
## Running
|
||||
|
||||
python client.py [-c CONFIGDIR] [-w WHITELIST]
|
||||
|
||||
@@ -11,40 +11,39 @@ Single-file Python application for managing LXST Channel connections via Reticul
|
||||
-w, --whitelist : path to bridge_whitelist.json
|
||||
-v, --verbose : enable debug logging
|
||||
--rnsconfig : alternative Reticulum config directory
|
||||
--whisplay, -W : enable Whisplay HAT GUI mode
|
||||
|
||||
## Running (Whisplay GUI mode)
|
||||
|
||||
python client.py -W [-c CONFIGDIR] [-w WHITELIST]
|
||||
|
||||
## CLI Commands
|
||||
|
||||
Inside the interactive client:
|
||||
d Discover channels from whitelist
|
||||
l List discovered channels
|
||||
<NUM> Connect to channel at index NUM (1-based)
|
||||
h / ? Show help
|
||||
q Quit
|
||||
|
||||
## Whisplay GUI Controls
|
||||
|
||||
### Channel list (STATE_READY)
|
||||
|
||||
Short press (<1s) Cycle to next channel in list
|
||||
Long press (>=1s) Call the selected channel
|
||||
Press (while in call) Hang up
|
||||
|
||||
RGB LED indicates state:
|
||||
### In call — Push-to-Talk mode (default)
|
||||
|
||||
Hold (>=1s) Talk (unmute microphone)
|
||||
Release Stop talking (mute microphone)
|
||||
Short press (<1s) Switch to always-on mode
|
||||
Double press Hang up
|
||||
|
||||
### In call — Always-on mode (toggled)
|
||||
|
||||
Short press (<1s) Switch to push-to-talk mode
|
||||
Double press Hang up
|
||||
|
||||
### RGB LED indicates state:
|
||||
Blue = ready, showing channels
|
||||
Orange = calling
|
||||
Green = in call
|
||||
Yellow = transmitting (PTT mode, mic active)
|
||||
|
||||
## Initial Setup Order
|
||||
|
||||
1. Ensure ~/.lxst-client or /etc/lxst-client exists
|
||||
2. Place bridge_whitelist.json in either directory (or pass with -w)
|
||||
3. First run creates identity automatically at configdir/identity
|
||||
4. In CLI mode: run `python client.py` then `d` to discover channels
|
||||
5. In GUI mode: run `python client.py -W` — auto-discovers on startup
|
||||
6. Messages appear in configdir/lxmf/
|
||||
4. In GUI mode: run `python client.py -W` — auto-discovers on startup
|
||||
5. Messages appear in configdir/lxmf/
|
||||
|
||||
## Known quirks
|
||||
|
||||
@@ -53,4 +52,3 @@ Single-file Python application for managing LXST Channel connections via Reticul
|
||||
- Use -v for RNS debug output (path discovery is verbose here).
|
||||
- Path discovery waits up to 10s for relays; increase timeout in client code if needed.
|
||||
- Whisplay mode requires Pillow and the Whisplay runtime (whisplay.py / whisplay_client.py).
|
||||
- Falls back to CLI gracefully if Whisplay hardware or dependencies are unavailable.
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
[
|
||||
"8567367936f085f93bf4ecdf494fafd0"
|
||||
"8567367936f085f93bf4ecdf494fafd0", "c64bc5a948096bd624bd9c186c35bdb1"
|
||||
]
|
||||
|
||||
@@ -7,36 +7,14 @@ import time
|
||||
import threading
|
||||
import signal
|
||||
import argparse
|
||||
|
||||
import subprocess
|
||||
|
||||
import RNS
|
||||
import LXMF
|
||||
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
from LXST.Primitives.Telephony import Telephone
|
||||
|
||||
|
||||
# ── Whisplay HAT GUI support ──────────────────────────────────────────
|
||||
_RUNTIME_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "runtime")
|
||||
if os.path.isdir(_RUNTIME_DIR):
|
||||
sys.path.insert(0, _RUNTIME_DIR)
|
||||
|
||||
_WHISPLAY_AVAILABLE = False
|
||||
try:
|
||||
from whisplay_client import create_whisplay_hardware
|
||||
_WHISPLAY_AVAILABLE = True
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
_HAS_PIL = False
|
||||
if _WHISPLAY_AVAILABLE:
|
||||
try:
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
_HAS_PIL = True
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
LONG_PRESS_THRESHOLD = 1.0
|
||||
|
||||
from whisplay_interface import WhisplayInterface
|
||||
|
||||
class Terminal:
|
||||
if not RNS.vendor.platformutils.is_windows():
|
||||
@@ -57,7 +35,7 @@ class ChannelClient:
|
||||
STATE_IN_CALL = 4
|
||||
|
||||
def __init__(self, configdir=None, rnsconfigdir=None, whitelist_path=None,
|
||||
verbosity=0, announce_interval=0, use_whisplay=False):
|
||||
verbosity=0, announce_interval=0):
|
||||
self.state = self.STATE_IDLE
|
||||
self.channels = []
|
||||
self.telephone = None
|
||||
@@ -67,16 +45,13 @@ class ChannelClient:
|
||||
self._response_event = threading.Event()
|
||||
|
||||
# ── Whisplay HAT init ──
|
||||
self.board = None
|
||||
self.selected_index = 0
|
||||
self._press_time = 0.0
|
||||
self._wm8960_device = None
|
||||
if use_whisplay and _WHISPLAY_AVAILABLE and _HAS_PIL:
|
||||
try:
|
||||
self._init_whisplay()
|
||||
except Exception as e:
|
||||
RNS.log(f"Whisplay init failed: {e}", RNS.LOG_WARNING)
|
||||
self.board = None
|
||||
self._ptt_enabled = True
|
||||
self._ptt_active = False
|
||||
self.whisplay_interface = None
|
||||
|
||||
self._init_whisplay()
|
||||
|
||||
if configdir is None:
|
||||
if os.path.isdir("/etc/lxst-client"):
|
||||
@@ -140,25 +115,21 @@ class ChannelClient:
|
||||
# ── Whisplay HAT GUI ─────────────────────────────────────────────
|
||||
|
||||
def _init_whisplay(self):
|
||||
from whisplay_client import create_whisplay_hardware
|
||||
|
||||
self.board = create_whisplay_hardware(
|
||||
app_id="lxst-client",
|
||||
display_name="LXST Client",
|
||||
icon="LC",
|
||||
use_daemon_default_log=True,
|
||||
)
|
||||
self.board.set_backlight(70)
|
||||
self.board.on_button_press(self._on_button_press)
|
||||
self.board.on_button_release(self._on_button_release)
|
||||
if hasattr(self.board, "on_exit_request"):
|
||||
self.board.on_exit_request(self._on_exit_request)
|
||||
if hasattr(self.board, "on_focus_revoked"):
|
||||
self.board.on_focus_revoked(self._on_focus_revoked)
|
||||
if hasattr(self.board, "start_event_listener"):
|
||||
self.board.start_event_listener()
|
||||
self.whisplay_interface = WhisplayInterface()
|
||||
self.whisplay_interface.create_board()
|
||||
self.whisplay_interface.board.set_backlight(70)
|
||||
if hasattr(self.whisplay_interface.board, "on_exit_request"):
|
||||
self.whisplay_interface.board.on_exit_request(self._on_exit_request)
|
||||
if hasattr(self.whisplay_interface.board, "on_focus_revoked"):
|
||||
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:
|
||||
@@ -242,7 +213,7 @@ class ChannelClient:
|
||||
return lines
|
||||
|
||||
def _render(self, title, body_lines, accent=(60, 150, 255), footer=""):
|
||||
w, h = self.board.LCD_WIDTH, self.board.LCD_HEIGHT
|
||||
w, h = self.whisplay_interface.board.LCD_WIDTH, self.whisplay_interface.board.LCD_HEIGHT
|
||||
img = Image.new("RGB", (w, h), (11, 16, 24))
|
||||
draw = ImageDraw.Draw(img)
|
||||
draw.rounded_rectangle((12, 16, w - 12, h - 16), radius=18,
|
||||
@@ -278,13 +249,13 @@ class ChannelClient:
|
||||
return self._rgb565(img)
|
||||
|
||||
def _flash_screen(self):
|
||||
if self.board is None:
|
||||
if self.whisplay_interface.board is None:
|
||||
return
|
||||
fb = self._render("LXST Client", ["Channels loading..."], footer="Please wait")
|
||||
self.board.draw_image(0, 0, self.board.LCD_WIDTH, self.board.LCD_HEIGHT, fb)
|
||||
self.whisplay_interface.board.draw_image(0, 0, self.whisplay_interface.board.LCD_WIDTH, self.whisplay_interface.board.LCD_HEIGHT, fb)
|
||||
|
||||
def _show_channels(self):
|
||||
if self.board is None:
|
||||
if self.whisplay_interface.board is None:
|
||||
return
|
||||
if not self.channels:
|
||||
fb = self._render("LXST Client", ["No channels", "", "Re-discover to find channels"],
|
||||
@@ -295,49 +266,96 @@ class ChannelClient:
|
||||
lines.append((f"{i+1}: {ch['hash'][:16]}..", i == self.selected_index))
|
||||
fb = self._render(f"Channels ({len(self.channels)})", lines,
|
||||
footer="Short: next | Long: select")
|
||||
self.board.draw_image(0, 0, self.board.LCD_WIDTH, self.board.LCD_HEIGHT, fb)
|
||||
self.whisplay_interface.board.draw_image(0, 0, self.whisplay_interface.board.LCD_WIDTH, self.whisplay_interface.board.LCD_HEIGHT, fb)
|
||||
|
||||
def _show_in_call(self):
|
||||
if self.board is None:
|
||||
if self.whisplay_interface.board is None:
|
||||
return
|
||||
ch = self.channels[self.selected_index] if self.channels else None
|
||||
lines = [f"Channel {self.selected_index + 1}"]
|
||||
if ch:
|
||||
lines.append(ch["hash"][:16] + "..")
|
||||
lines += ["", "Press to hang up"]
|
||||
fb = self._render("In Call", lines, accent=(60, 220, 120), footer="Press button to end")
|
||||
self.board.draw_image(0, 0, self.board.LCD_WIDTH, self.board.LCD_HEIGHT, fb)
|
||||
if self._ptt_enabled:
|
||||
lines += ["", "PTT: hold to talk", "release to mute"]
|
||||
footer = "Short: always-on | Dbl: leave"
|
||||
else:
|
||||
lines += ["", "Always-on (unmuted)"]
|
||||
footer = "Short: PTT Dbl: leave"
|
||||
fb = self._render("In Call", lines, accent=(60, 220, 120), footer=footer)
|
||||
self.whisplay_interface.board.draw_image(0, 0, self.whisplay_interface.board.LCD_WIDTH, self.whisplay_interface.board.LCD_HEIGHT, fb)
|
||||
|
||||
def _show_status(self, title, detail="", accent=(255, 180, 60)):
|
||||
if self.board is None:
|
||||
if self.whisplay_interface.board is None:
|
||||
return
|
||||
lines = [detail] if detail else []
|
||||
fb = self._render(title, lines, accent=accent)
|
||||
self.board.draw_image(0, 0, self.board.LCD_WIDTH, self.board.LCD_HEIGHT, fb)
|
||||
self.whisplay_interface.board.draw_image(0, 0, self.whisplay_interface.board.LCD_WIDTH, self.whisplay_interface.board.LCD_HEIGHT, fb)
|
||||
|
||||
def _on_button_press(self):
|
||||
self._press_time = time.time()
|
||||
|
||||
def _on_button_release(self):
|
||||
elapsed = time.time() - self._press_time
|
||||
def _on_press(self):
|
||||
if self.state == self.STATE_IN_CALL:
|
||||
self.telephone.hangup()
|
||||
return
|
||||
if self.state != self.STATE_READY:
|
||||
return
|
||||
if elapsed >= LONG_PRESS_THRESHOLD:
|
||||
if self.channels:
|
||||
self.board.set_rgb(255, 180, 0)
|
||||
self.call_channel(self.selected_index)
|
||||
else:
|
||||
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)
|
||||
|
||||
def _on_double_press(self):
|
||||
if self.state == self.STATE_READY:
|
||||
self._on_press()
|
||||
|
||||
def _on_long_press(self):
|
||||
pass
|
||||
|
||||
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()
|
||||
|
||||
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)
|
||||
|
||||
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()
|
||||
if self.board:
|
||||
self.board.prepare_exit()
|
||||
if self.whisplay_interface.board:
|
||||
self.whisplay_interface.board.prepare_exit()
|
||||
|
||||
def _on_focus_revoked(self, payload=None):
|
||||
self.cleanup()
|
||||
@@ -457,25 +475,41 @@ class ChannelClient:
|
||||
f"\nCall established with "
|
||||
f"{RNS.prettyhexrep(remote_identity.hash)}"
|
||||
)
|
||||
if self.board:
|
||||
self.board.set_rgb(0, 255, 80)
|
||||
if self._ptt_enabled:
|
||||
self._hw_set_mic_muted(True)
|
||||
if self.whisplay_interface.board:
|
||||
self.whisplay_interface.board.set_rgb(0, 255, 80)
|
||||
self._show_in_call()
|
||||
|
||||
def _on_call_ended(self, remote_identity):
|
||||
was_in_call = self.state == self.STATE_IN_CALL
|
||||
was_calling = self.state == self.STATE_CALLING
|
||||
self.state = self.STATE_READY
|
||||
self._ptt_active = False
|
||||
if was_in_call:
|
||||
print(f"\nCall ended")
|
||||
elif was_calling:
|
||||
print(f"\nCall could not be connected")
|
||||
if self.board:
|
||||
self.board.set_rgb(0, 80, 255)
|
||||
if self.whisplay_interface.board:
|
||||
self.whisplay_interface.board.set_rgb(0, 80, 255)
|
||||
if was_in_call:
|
||||
self._show_status("Call Ended")
|
||||
time.sleep(1)
|
||||
self._show_channels()
|
||||
|
||||
def _hw_set_mic_muted(self, muted):
|
||||
if self._wm8960_device is None:
|
||||
return
|
||||
card = self._wm8960_device.split(",")[0].split(":")[-1]
|
||||
state = "off" if muted else "on"
|
||||
try:
|
||||
subprocess.run(
|
||||
["amixer", "-c", card, "cset", "name='Capture Switch'", state],
|
||||
check=False, capture_output=True, timeout=2,
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def call_channel(self, index):
|
||||
if index < 0 or index >= len(self.channels):
|
||||
print("Invalid channel index")
|
||||
@@ -549,16 +583,13 @@ class ChannelClient:
|
||||
signal.signal(signal.SIGTERM, self._sigterm_handler)
|
||||
self.should_run = True
|
||||
|
||||
if self.board:
|
||||
self._run_gui()
|
||||
else:
|
||||
self._run_cli()
|
||||
self._run_gui()
|
||||
|
||||
def _run_gui(self):
|
||||
self._flash_screen()
|
||||
self.discover(timeout=15)
|
||||
self.selected_index = 0
|
||||
self.board.set_rgb(0, 80, 255)
|
||||
self.whisplay_interface.board.set_rgb(0, 80, 255)
|
||||
self._show_channels()
|
||||
while self.should_run:
|
||||
if self.state == self.STATE_CALLING:
|
||||
@@ -568,75 +599,12 @@ class ChannelClient:
|
||||
continue
|
||||
time.sleep(0.1)
|
||||
|
||||
def _run_cli(self):
|
||||
print(
|
||||
f"\n{Terminal.BOLD}"
|
||||
f"LXST Channel Client"
|
||||
f"{Terminal.END}"
|
||||
)
|
||||
print(f" Identity: {RNS.prettyhexrep(self.identity.hash)}")
|
||||
print(f" Whitelist: {self.whitelist_path}")
|
||||
print()
|
||||
|
||||
self.discover(timeout=15)
|
||||
self.show_channels()
|
||||
self.show_help()
|
||||
|
||||
while self.should_run:
|
||||
if self.state == self.STATE_IN_CALL:
|
||||
try:
|
||||
input(" In call, press Enter to hang up > ")
|
||||
self.telephone.hangup()
|
||||
self.state = self.STATE_READY
|
||||
except (EOFError, KeyboardInterrupt):
|
||||
break
|
||||
continue
|
||||
|
||||
if self.state == self.STATE_CALLING:
|
||||
time.sleep(0.5)
|
||||
continue
|
||||
|
||||
if self.state == self.STATE_DISCOVERING:
|
||||
time.sleep(0.5)
|
||||
continue
|
||||
|
||||
try:
|
||||
inp = input("> ").strip().lower()
|
||||
except (EOFError, KeyboardInterrupt):
|
||||
print()
|
||||
break
|
||||
|
||||
if not inp:
|
||||
continue
|
||||
|
||||
if inp == "q":
|
||||
break
|
||||
elif inp in ("h", "?"):
|
||||
self.show_help()
|
||||
elif inp == "d":
|
||||
self.discover()
|
||||
self.show_channels()
|
||||
elif inp == "l":
|
||||
self.show_channels()
|
||||
else:
|
||||
try:
|
||||
idx = int(inp) - 1
|
||||
if 0 <= idx < len(self.channels):
|
||||
self.call_channel(idx)
|
||||
else:
|
||||
print(
|
||||
f"Invalid index. "
|
||||
f"Enter a number between 1 and {len(self.channels)}"
|
||||
)
|
||||
except ValueError:
|
||||
print(f"Unknown command: {inp}. Type 'h' for help.")
|
||||
|
||||
def cleanup(self):
|
||||
if self.telephone and self.state == self.STATE_IN_CALL:
|
||||
self.telephone.hangup()
|
||||
self.should_run = False
|
||||
if self.board:
|
||||
self.board.cleanup()
|
||||
if self.whisplay_interface.board:
|
||||
self.whisplay_interface.board.cleanup()
|
||||
|
||||
def _sigint_handler(self, sig, frame):
|
||||
self.cleanup()
|
||||
@@ -672,11 +640,6 @@ def main():
|
||||
help="seconds between LXMF destination announces (0 = announce once at startup, default)",
|
||||
type=int,
|
||||
)
|
||||
parser.add_argument(
|
||||
"--whisplay", "-W",
|
||||
action="store_true",
|
||||
help="enable Whisplay HAT GUI mode",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
client = ChannelClient(
|
||||
@@ -685,7 +648,6 @@ def main():
|
||||
whitelist_path=args.whitelist,
|
||||
verbosity=args.verbose,
|
||||
announce_interval=args.announce_interval,
|
||||
use_whisplay=args.whisplay,
|
||||
)
|
||||
|
||||
try:
|
||||
|
||||
@@ -4,7 +4,7 @@ import socket
|
||||
import threading
|
||||
import time
|
||||
|
||||
from whisplay import WhisplayBoard
|
||||
from runtime.whisplay import WhisplayBoard
|
||||
|
||||
|
||||
DEFAULT_DAEMON_SOCKET_PATH = "/tmp/whisplay-daemon.sock"
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
import subprocess
|
||||
import time
|
||||
import threading
|
||||
import os
|
||||
import runtime.whisplay_client
|
||||
|
||||
|
||||
class WhisplayInterface:
|
||||
LONG_PRESS_THRESHOLD = 0.5
|
||||
DOUBLE_PRESS_THRESHOLD = 0.3
|
||||
|
||||
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._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
|
||||
|
||||
self.set_wm8960_volume_stable(speaker=200, mic=80)
|
||||
|
||||
def create_board(self):
|
||||
self.board = runtime.whisplay_client.create_whisplay_hardware(
|
||||
app_id=os.getenv("WHISPLAY_APP_ID", "whisplay-lxst-client"),
|
||||
display_name="LXST Client",
|
||||
icon="L",
|
||||
exit_gesture="quad_press",
|
||||
use_daemon_default_log=False,
|
||||
)
|
||||
self.board.on_button_press(self._on_button_press)
|
||||
self.board.on_button_release(self._on_button_release)
|
||||
|
||||
# -- 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._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._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()
|
||||
|
||||
def _long_press_timer(self):
|
||||
with self._lock:
|
||||
if self._is_pressing:
|
||||
self._is_long_pressing = True
|
||||
if self._on_long_press_callback:
|
||||
self._on_long_press_callback()
|
||||
|
||||
|
||||
def set_wm8960_volume_stable(self, speaker: int = 0, mic: int = 0):
|
||||
CARD_NAME = 'wm8960soundcard'
|
||||
DEVICE_ARG = f'hw:{CARD_NAME}'
|
||||
try:
|
||||
subprocess.run(['amixer', '-D', DEVICE_ARG, 'sset', 'Speaker',
|
||||
str(speaker)], check=False, capture_output=True)
|
||||
subprocess.run(['amixer', '-D', DEVICE_ARG, 'sset',
|
||||
'Capture', str(mic)], check=False, capture_output=True)
|
||||
except Exception as e:
|
||||
print(f"ERROR: Failed to set volume: {e}")
|
||||
Reference in New Issue
Block a user