601 lines
21 KiB
Python
601 lines
21 KiB
Python
|
|
import platform
|
||
|
|
import threading
|
||
|
|
import time
|
||
|
|
from abc import ABC, ABCMeta, abstractmethod
|
||
|
|
import threading
|
||
|
|
from abc import ABC, ABCMeta, abstractmethod
|
||
|
|
import ctypes
|
||
|
|
import ctypes.util
|
||
|
|
import threading
|
||
|
|
import traceback
|
||
|
|
from typing import Optional
|
||
|
|
|
||
|
|
from ..FeatrueOption import FeatrueOption, device_type
|
||
|
|
from ..Transport.LibUSBHIDAPI import LibUSBHIDAPI
|
||
|
|
from ..InputTypes import InputEvent, ButtonKey
|
||
|
|
|
||
|
|
|
||
|
|
class TransportError(Exception):
|
||
|
|
"""Custom exception type for transport errors"""
|
||
|
|
|
||
|
|
def __init__(self, message, code=None):
|
||
|
|
super().__init__(message)
|
||
|
|
self.code = code # Optional error code
|
||
|
|
|
||
|
|
def __str__(self):
|
||
|
|
if self.code:
|
||
|
|
return f"[Error Code {self.code}] {super().__str__()}"
|
||
|
|
return super().__str__()
|
||
|
|
|
||
|
|
|
||
|
|
class StreamDock(ABC):
|
||
|
|
"""
|
||
|
|
Represents a physically attached StreamDock device.
|
||
|
|
"""
|
||
|
|
|
||
|
|
KEY_COUNT = 0
|
||
|
|
KEY_COLS = 0
|
||
|
|
KEY_ROWS = 0
|
||
|
|
|
||
|
|
KEY_PIXEL_WIDTH = 0
|
||
|
|
KEY_PIXEL_HEIGHT = 0
|
||
|
|
KEY_IMAGE_FORMAT = ""
|
||
|
|
KEY_FLIP = (False, False)
|
||
|
|
KEY_ROTATION = 0
|
||
|
|
KEY_MAP = False
|
||
|
|
|
||
|
|
TOUCHSCREEN_PIXEL_WIDTH = 0
|
||
|
|
TOUCHSCREEN_PIXEL_HEIGHT = 0
|
||
|
|
TOUCHSCREEN_IMAGE_FORMAT = ""
|
||
|
|
TOUCHSCREEN_FLIP = (False, False)
|
||
|
|
TOUCHSCREEN_ROTATION = 0
|
||
|
|
|
||
|
|
DIAL_COUNT = 0
|
||
|
|
|
||
|
|
DECK_TYPE = ""
|
||
|
|
DECK_VISUAL = False
|
||
|
|
DECK_TOUCH = False
|
||
|
|
|
||
|
|
transport: LibUSBHIDAPI
|
||
|
|
screenlicent = None
|
||
|
|
__metaclass__ = ABCMeta
|
||
|
|
__seconds = 300
|
||
|
|
|
||
|
|
feature_option: FeatrueOption
|
||
|
|
|
||
|
|
def __init__(self, transport1: LibUSBHIDAPI, devInfo):
|
||
|
|
self.transport = transport1
|
||
|
|
self.vendor_id = devInfo["vendor_id"]
|
||
|
|
self.product_id = devInfo["product_id"]
|
||
|
|
self.path = devInfo["path"]
|
||
|
|
self.serial_number = devInfo.get("serial_number", "")
|
||
|
|
self.firmware_version = ""
|
||
|
|
self.read_thread = None
|
||
|
|
self.run_read_thread = False
|
||
|
|
self.feature_option = FeatrueOption()
|
||
|
|
self.key_callback = None
|
||
|
|
# CRITICAL: Add lock to protect callback access in multi-threaded environment
|
||
|
|
self._callback_lock = threading.Lock()
|
||
|
|
# Heartbeat thread for keeping device alive
|
||
|
|
self.heartbeat_thread = None
|
||
|
|
self.run_heartbeat_thread = False
|
||
|
|
|
||
|
|
# self.update_lock = threading.RLock()
|
||
|
|
# self.screenlicent=threading.Timer(self.__seconds,self.screen_Off)
|
||
|
|
# self.screenlicent.start()
|
||
|
|
|
||
|
|
def __del__(self):
|
||
|
|
"""
|
||
|
|
Delete handler for the StreamDock, automatically closing the transport
|
||
|
|
if it is currently open and terminating the transport reader thread.
|
||
|
|
|
||
|
|
CRITICAL: This is called during garbage collection which may happen during
|
||
|
|
interpreter shutdown. We need to be extremely careful to avoid calling
|
||
|
|
C code during shutdown as it can cause segmentation faults.
|
||
|
|
"""
|
||
|
|
import sys
|
||
|
|
|
||
|
|
# CRITICAL: Don't call C code during interpreter shutdown
|
||
|
|
if sys.is_finalizing():
|
||
|
|
# During interpreter shutdown, skip cleanup to avoid segfault
|
||
|
|
# The OS will clean up resources when process exits
|
||
|
|
return
|
||
|
|
|
||
|
|
try:
|
||
|
|
# Stop the reader thread (safe operation)
|
||
|
|
self.run_read_thread = False
|
||
|
|
if self.read_thread and self.read_thread.is_alive():
|
||
|
|
self.read_thread.join(timeout=0.5) # Short timeout during __del__
|
||
|
|
except (TransportError, ValueError, RuntimeError):
|
||
|
|
pass
|
||
|
|
|
||
|
|
try:
|
||
|
|
# Stop the heartbeat thread (safe operation)
|
||
|
|
self.run_heartbeat_thread = False
|
||
|
|
if self.heartbeat_thread and self.heartbeat_thread.is_alive():
|
||
|
|
self.heartbeat_thread.join(timeout=0.5) # Short timeout during __del__
|
||
|
|
except (TransportError, ValueError, RuntimeError):
|
||
|
|
pass
|
||
|
|
|
||
|
|
try:
|
||
|
|
# Close transport - this may call C code but we checked is_finalizing above
|
||
|
|
self.close()
|
||
|
|
except TransportError:
|
||
|
|
pass
|
||
|
|
|
||
|
|
def __enter__(self):
|
||
|
|
"""
|
||
|
|
Enter handler for the StreamDock, taking the exclusive update lock on
|
||
|
|
the deck. This can be used in a `with` statement to ensure that only one
|
||
|
|
thread is currently updating the deck, even if it is doing multiple
|
||
|
|
operations (e.g. setting the image on multiple keys).
|
||
|
|
"""
|
||
|
|
# self.update_lock.acquire()
|
||
|
|
|
||
|
|
def __exit__(self, type, value, traceback):
|
||
|
|
"""
|
||
|
|
Exit handler for the StreamDock, releasing the exclusive update lock on
|
||
|
|
the deck.
|
||
|
|
"""
|
||
|
|
# self.update_lock.release()
|
||
|
|
|
||
|
|
# Open device
|
||
|
|
def open(self):
|
||
|
|
res1 = self.transport.open(bytes(self.path, "utf-8"))
|
||
|
|
self._setup_reader(self._read)
|
||
|
|
# Start heartbeat with delay to avoid Linux libusb deadlock
|
||
|
|
# The read thread needs time to initialize before heartbeat starts
|
||
|
|
time.sleep(0.1)
|
||
|
|
self._start_heartbeat()
|
||
|
|
# macOS need to get firmware version after opening device
|
||
|
|
if platform.system() == "Darwin":
|
||
|
|
self.firmware_version = self.transport.get_firmware_version()
|
||
|
|
return res1
|
||
|
|
|
||
|
|
# Initialize
|
||
|
|
def init(self):
|
||
|
|
self.set_device()
|
||
|
|
self.wakeScreen()
|
||
|
|
self.set_brightness(100)
|
||
|
|
self.clearAllIcon()
|
||
|
|
if platform.system() != "Darwin":
|
||
|
|
self.firmware_version = self.transport.get_firmware_version()
|
||
|
|
self.refresh()
|
||
|
|
|
||
|
|
# Set device parameters
|
||
|
|
@abstractmethod
|
||
|
|
def set_device(self):
|
||
|
|
pass
|
||
|
|
|
||
|
|
# Set device LED brightness
|
||
|
|
def set_led_brightness(self, percent):
|
||
|
|
if self.feature_option.hasRGBLed:
|
||
|
|
return self.transport.set_led_brightness(percent)
|
||
|
|
|
||
|
|
# Set device LED color
|
||
|
|
def set_led_color(self, r, g, b):
|
||
|
|
if self.feature_option.hasRGBLed:
|
||
|
|
return self.transport.set_led_color(self.feature_option.ledCounts, r, g, b)
|
||
|
|
|
||
|
|
# Reset device LED effects
|
||
|
|
def reset_led_effect(self):
|
||
|
|
if self.feature_option.hasRGBLed:
|
||
|
|
return self.transport.reset_led_color()
|
||
|
|
|
||
|
|
# Close device
|
||
|
|
def close(self):
|
||
|
|
"""
|
||
|
|
Close the device and release all resources.
|
||
|
|
|
||
|
|
CRITICAL: This method must be called before the object is destroyed to ensure
|
||
|
|
clean shutdown of the C library and prevent segmentation faults.
|
||
|
|
"""
|
||
|
|
# print(f"[DEBUG] Closing device: {self.path}")
|
||
|
|
|
||
|
|
# CRITICAL: Stop heartbeat thread first
|
||
|
|
self.run_heartbeat_thread = False
|
||
|
|
if self.heartbeat_thread and self.heartbeat_thread.is_alive():
|
||
|
|
try:
|
||
|
|
self.heartbeat_thread.join(timeout=2.0)
|
||
|
|
except Exception as e:
|
||
|
|
print(f"[WARNING] Error while waiting for heartbeat thread to exit: {e}", flush=True)
|
||
|
|
|
||
|
|
# CRITICAL: Stop reader thread first and wait for it to finish
|
||
|
|
self.run_read_thread = False
|
||
|
|
|
||
|
|
if self.read_thread and self.read_thread.is_alive():
|
||
|
|
try:
|
||
|
|
# Give thread time to exit naturally
|
||
|
|
self.read_thread.join(timeout=2.0) # Wait up to 2 seconds
|
||
|
|
if self.read_thread.is_alive():
|
||
|
|
print("[WARNING] Read thread did not exit in time", flush=True)
|
||
|
|
except Exception as e:
|
||
|
|
print(f"[WARNING] Error while waiting for read thread to exit: {e}", flush=True)
|
||
|
|
|
||
|
|
# Send disconnect command (may fail if device already disconnected)
|
||
|
|
try:
|
||
|
|
self.disconnected()
|
||
|
|
except Exception as e:
|
||
|
|
print(f"[WARNING] Error sending disconnect command: {e}", flush=True)
|
||
|
|
|
||
|
|
# CRITICAL: Close transport properly to release HID device
|
||
|
|
try:
|
||
|
|
self.transport.close()
|
||
|
|
except Exception as e:
|
||
|
|
print(f"[WARNING] Error closing transport: {e}", flush=True)
|
||
|
|
|
||
|
|
# Clear callback to break any circular references
|
||
|
|
with self._callback_lock:
|
||
|
|
self.key_callback = None
|
||
|
|
|
||
|
|
# print("[DEBUG] Device closed")
|
||
|
|
|
||
|
|
# Disconnect and clear all displays
|
||
|
|
def disconnected(self):
|
||
|
|
self.transport.disconnected()
|
||
|
|
|
||
|
|
# Clear a specific key icon
|
||
|
|
def clearIcon(self, index):
|
||
|
|
origin = index
|
||
|
|
if origin not in range(1, self.KEY_COUNT + 1):
|
||
|
|
print(f"key '{origin}' out of range. you should set (1 ~ {self.KEY_COUNT})")
|
||
|
|
return -1
|
||
|
|
logical_key = ButtonKey(origin) if isinstance(origin, int) else origin
|
||
|
|
hardware_key = self.get_image_key(logical_key)
|
||
|
|
self.transport.keyClear(hardware_key)
|
||
|
|
|
||
|
|
# Clear all key icons
|
||
|
|
def clearAllIcon(self):
|
||
|
|
self.transport.keyAllClear()
|
||
|
|
|
||
|
|
# Wake the screen
|
||
|
|
def wakeScreen(self):
|
||
|
|
self.transport.wakeScreen()
|
||
|
|
|
||
|
|
# Refresh the device display
|
||
|
|
def refresh(self):
|
||
|
|
self.transport.refresh()
|
||
|
|
|
||
|
|
# Get device path
|
||
|
|
def getPath(self):
|
||
|
|
return self.path
|
||
|
|
|
||
|
|
# Get device feedback data
|
||
|
|
def read(self):
|
||
|
|
"""
|
||
|
|
:argtypes: byte array to store info; recommended length 1024
|
||
|
|
|
||
|
|
"""
|
||
|
|
data = self.transport.read_(1024)
|
||
|
|
return data
|
||
|
|
|
||
|
|
# Continuously check for device feedback; recommended to run in a thread
|
||
|
|
def whileread(self):
|
||
|
|
"""
|
||
|
|
@deprecated Use the built-in async callback mechanism instead of calling this directly
|
||
|
|
"""
|
||
|
|
from ..InputTypes import EventType
|
||
|
|
|
||
|
|
while 1:
|
||
|
|
try:
|
||
|
|
data = self.read()
|
||
|
|
if data != None and len(data) >= 11:
|
||
|
|
try:
|
||
|
|
event = self.decode_input_event(data[9], data[10])
|
||
|
|
if event.event_type == EventType.BUTTON:
|
||
|
|
action = "pressed" if event.state == 1 else "released"
|
||
|
|
print(
|
||
|
|
f"Key {event.key.value if event.key else '?'} was {action}"
|
||
|
|
)
|
||
|
|
elif event.event_type == EventType.KNOB_ROTATE:
|
||
|
|
print(
|
||
|
|
f"Knob {event.knob_id.value if event.knob_id else '?'} rotated {event.direction.value if event.direction else '?'}"
|
||
|
|
)
|
||
|
|
elif event.event_type == EventType.KNOB_PRESS:
|
||
|
|
action = "pressed" if event.state == 1 else "released"
|
||
|
|
print(
|
||
|
|
f"Knob {event.knob_id.value if event.knob_id else '?'} was {action}"
|
||
|
|
)
|
||
|
|
elif event.event_type == EventType.SWIPE:
|
||
|
|
print(
|
||
|
|
f"Swipe gesture: {event.direction.value if event.direction else '?'}"
|
||
|
|
)
|
||
|
|
except Exception:
|
||
|
|
pass
|
||
|
|
# self.transport.deleteRead()
|
||
|
|
except Exception as e:
|
||
|
|
print("Error occurred:")
|
||
|
|
traceback.print_exc() # Print detailed exception info
|
||
|
|
break
|
||
|
|
|
||
|
|
# # Screen off
|
||
|
|
# def screen_Off(self):
|
||
|
|
# res=self.transport.screen_Off()
|
||
|
|
# self.reset_Countdown(self.__seconds)
|
||
|
|
# return res
|
||
|
|
# # Wake screen
|
||
|
|
# def screen_On(self):
|
||
|
|
# return self.transport.screen_On()
|
||
|
|
# # Set timer interval
|
||
|
|
# def set_seconds(self, data):
|
||
|
|
# self.__seconds = data
|
||
|
|
# self.reset_Countdown(self.__seconds)
|
||
|
|
|
||
|
|
# # Restart timer
|
||
|
|
# def reset_Countdown(self, data):
|
||
|
|
# if self.screenlicent is not None:
|
||
|
|
# self.screenlicent.cancel()
|
||
|
|
# if hasattr(self, 'screen_Off'):
|
||
|
|
# self.screenlicent = threading.Timer(data, self.screen_Off)
|
||
|
|
# self.screenlicent.start()
|
||
|
|
|
||
|
|
def get_serial_number(self):
|
||
|
|
"""Return the device serial number."""
|
||
|
|
return self.serial_number
|
||
|
|
|
||
|
|
|
||
|
|
@abstractmethod
|
||
|
|
def set_key_image(self, key, path) -> int | None:
|
||
|
|
pass
|
||
|
|
|
||
|
|
# @abstractmethod
|
||
|
|
# def set_key_imageData(self, key, image, width=126, height=126):
|
||
|
|
# pass
|
||
|
|
|
||
|
|
@abstractmethod
|
||
|
|
def set_brightness(self, percent):
|
||
|
|
pass
|
||
|
|
|
||
|
|
@abstractmethod
|
||
|
|
def set_touchscreen_image(self, path) -> int | None:
|
||
|
|
pass
|
||
|
|
|
||
|
|
def set_background_image(self, path) -> int | None:
|
||
|
|
self.set_touchscreen_image(path)
|
||
|
|
|
||
|
|
|
||
|
|
@abstractmethod
|
||
|
|
def get_image_key(self, logical_key: ButtonKey) -> int:
|
||
|
|
"""
|
||
|
|
Convert logical key value to hardware key value (for setting images)
|
||
|
|
|
||
|
|
Args:
|
||
|
|
logical_key: Logical key enum
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
int: Hardware key value
|
||
|
|
"""
|
||
|
|
pass
|
||
|
|
|
||
|
|
@abstractmethod
|
||
|
|
def decode_input_event(self, hardware_code: int, state: int) -> InputEvent:
|
||
|
|
"""
|
||
|
|
Decode hardware event codes into a unified InputEvent
|
||
|
|
|
||
|
|
Args:
|
||
|
|
hardware_code: Hardware event code
|
||
|
|
state: State (0=release, 1=press)
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
InputEvent: Decoded event object
|
||
|
|
"""
|
||
|
|
pass
|
||
|
|
|
||
|
|
def id(self):
|
||
|
|
"""
|
||
|
|
Retrieves the physical ID of the attached StreamDock. This can be used
|
||
|
|
to differentiate one StreamDock from another.
|
||
|
|
|
||
|
|
:rtype: str
|
||
|
|
:return: Identifier for the attached device.
|
||
|
|
"""
|
||
|
|
return self.getPath()
|
||
|
|
|
||
|
|
def _read(self):
|
||
|
|
try:
|
||
|
|
while self.run_read_thread:
|
||
|
|
try:
|
||
|
|
arr = self.read()
|
||
|
|
if arr is not None and len(arr) >= 10:
|
||
|
|
if arr[9] == 0xFF:
|
||
|
|
# Confirm write success
|
||
|
|
pass
|
||
|
|
else:
|
||
|
|
try:
|
||
|
|
# Use the device class event decoder
|
||
|
|
if self.feature_option.deviceType != device_type.k1pro:
|
||
|
|
event = self.decode_input_event(arr[9], arr[10])
|
||
|
|
else:
|
||
|
|
event = self.decode_input_event(arr[10], arr[11])
|
||
|
|
# Get callback reference with lock
|
||
|
|
with self._callback_lock:
|
||
|
|
callback = self.key_callback
|
||
|
|
|
||
|
|
# Call callback OUTSIDE of lock to avoid deadlocks
|
||
|
|
if callback is not None:
|
||
|
|
try:
|
||
|
|
# Callback signature: callback(device, event)
|
||
|
|
callback(self, event)
|
||
|
|
except Exception as callback_error:
|
||
|
|
print(
|
||
|
|
f"Key callback error: {callback_error}",
|
||
|
|
flush=True,
|
||
|
|
)
|
||
|
|
traceback.print_exc()
|
||
|
|
except Exception as decode_error:
|
||
|
|
print(f"Event decode error: {decode_error}", flush=True)
|
||
|
|
traceback.print_exc()
|
||
|
|
# else:
|
||
|
|
# print("read control", arr)
|
||
|
|
# Don't explicitly delete arr - let Python's GC handle it
|
||
|
|
# del arr causes issues with ctypes buffers on Linux
|
||
|
|
|
||
|
|
except Exception as e:
|
||
|
|
print(f"Error reading data: {e}", flush=True)
|
||
|
|
traceback.print_exc()
|
||
|
|
continue
|
||
|
|
except Exception as outer_error:
|
||
|
|
print(f"[FATAL] Read thread outer exception: {outer_error}", flush=True)
|
||
|
|
traceback.print_exc()
|
||
|
|
finally:
|
||
|
|
pass
|
||
|
|
|
||
|
|
def _heartbeat_worker(self):
|
||
|
|
"""
|
||
|
|
Worker method that sends heartbeat packets to the device every 10 seconds.
|
||
|
|
This keeps the device connection alive and prevents timeout.
|
||
|
|
"""
|
||
|
|
# Initial delay to allow device and read thread to stabilize
|
||
|
|
time.sleep(1.0)
|
||
|
|
try:
|
||
|
|
while self.run_heartbeat_thread:
|
||
|
|
try:
|
||
|
|
self.transport.heartbeat()
|
||
|
|
except Exception as e:
|
||
|
|
# Log but don't crash the thread on heartbeat errors
|
||
|
|
print(f"Heartbeat error: {e}", flush=True)
|
||
|
|
# Wait 10 seconds before next heartbeat
|
||
|
|
time.sleep(10)
|
||
|
|
except Exception as outer_error:
|
||
|
|
print(f"[FATAL] Heartbeat thread outer exception: {outer_error}", flush=True)
|
||
|
|
finally:
|
||
|
|
pass
|
||
|
|
|
||
|
|
def _setup_reader(self, callback):
|
||
|
|
"""
|
||
|
|
Sets up the internal transport reader thread with the given callback,
|
||
|
|
for asynchronous processing of HID events from the device. If the thread
|
||
|
|
already exists, it is terminated and restarted with the new callback
|
||
|
|
function.
|
||
|
|
|
||
|
|
:param function callback: Callback to run on the reader thread.
|
||
|
|
"""
|
||
|
|
if self.read_thread is not None:
|
||
|
|
self.run_read_thread = False
|
||
|
|
try:
|
||
|
|
self.read_thread.join()
|
||
|
|
# return
|
||
|
|
except RuntimeError:
|
||
|
|
pass
|
||
|
|
|
||
|
|
if callback is not None:
|
||
|
|
self.run_read_thread = True
|
||
|
|
self.read_thread = threading.Thread(target=callback)
|
||
|
|
self.read_thread.daemon = True
|
||
|
|
self.read_thread.start()
|
||
|
|
|
||
|
|
def _start_heartbeat(self):
|
||
|
|
"""
|
||
|
|
Starts the heartbeat thread that sends periodic heartbeat packets to the device.
|
||
|
|
"""
|
||
|
|
if self.heartbeat_thread is not None:
|
||
|
|
self.run_heartbeat_thread = False
|
||
|
|
try:
|
||
|
|
self.heartbeat_thread.join()
|
||
|
|
except RuntimeError:
|
||
|
|
pass
|
||
|
|
|
||
|
|
self.run_heartbeat_thread = True
|
||
|
|
self.heartbeat_thread = threading.Thread(target=self._heartbeat_worker)
|
||
|
|
self.heartbeat_thread.daemon = True
|
||
|
|
self.heartbeat_thread.start()
|
||
|
|
|
||
|
|
def set_key_callback(self, callback):
|
||
|
|
"""
|
||
|
|
Sets the callback function called each time a button on the StreamDock
|
||
|
|
changes state (either pressed, or released), or a knob is rotated/pressed,
|
||
|
|
or a swipe gesture is detected.
|
||
|
|
|
||
|
|
.. note:: This callback will be fired from an internal reader thread.
|
||
|
|
Ensure that the given callback function is thread-safe.
|
||
|
|
|
||
|
|
.. note:: Only one callback can be registered at one time.
|
||
|
|
|
||
|
|
.. seealso:: See :func:`~StreamDock.set_key_callback_async` method for
|
||
|
|
a version compatible with Python 3 `asyncio` asynchronous
|
||
|
|
functions.
|
||
|
|
|
||
|
|
:param function callback: Callback function with signature:
|
||
|
|
callback(device: StreamDock, event: InputEvent)
|
||
|
|
|
||
|
|
Example:
|
||
|
|
def on_input(device, event):
|
||
|
|
from StreamDock.InputTypes import EventType
|
||
|
|
if event.event_type == EventType.BUTTON:
|
||
|
|
print(f"Key {event.key.value} pressed")
|
||
|
|
elif event.event_type == EventType.KNOB_ROTATE:
|
||
|
|
print("Knob rotated")
|
||
|
|
"""
|
||
|
|
with self._callback_lock:
|
||
|
|
self.key_callback = callback
|
||
|
|
|
||
|
|
def set_key_callback_async(self, async_callback, loop=None):
|
||
|
|
"""
|
||
|
|
Sets the asynchronous callback function called each time a button on the
|
||
|
|
StreamDock changes state (either pressed, or released), or a knob is
|
||
|
|
rotated/pressed, or a swipe gesture is detected. The given callback
|
||
|
|
should be compatible with Python 3's `asyncio` routines.
|
||
|
|
|
||
|
|
.. note:: The asynchronous callback will be fired in a thread-safe
|
||
|
|
manner.
|
||
|
|
|
||
|
|
.. note:: This will override the callback (if any) set by
|
||
|
|
:func:`~StreamDock.set_key_callback`.
|
||
|
|
|
||
|
|
:param function async_callback: Asynchronous callback function with signature:
|
||
|
|
async_callback(device: StreamDock, event: InputEvent)
|
||
|
|
:param asyncio.loop loop: Asyncio loop to dispatch the callback into
|
||
|
|
"""
|
||
|
|
import asyncio
|
||
|
|
|
||
|
|
loop = loop or asyncio.get_event_loop()
|
||
|
|
|
||
|
|
def callback(*args):
|
||
|
|
asyncio.run_coroutine_threadsafe(async_callback(*args), loop)
|
||
|
|
|
||
|
|
self.set_key_callback(callback)
|
||
|
|
|
||
|
|
def set_touchscreen_callback(self, callback):
|
||
|
|
"""
|
||
|
|
Sets the callback function called each time there is an interaction
|
||
|
|
with a touchscreen on the StreamDock.
|
||
|
|
|
||
|
|
.. note:: This callback will be fired from an internal reader thread.
|
||
|
|
Ensure that the given callback function is thread-safe.
|
||
|
|
|
||
|
|
.. note:: Only one callback can be registered at one time.
|
||
|
|
|
||
|
|
.. seealso:: See :func:`~StreamDock.set_touchscreen_callback_async`
|
||
|
|
method for a version compatible with Python 3 `asyncio`
|
||
|
|
asynchronous functions.
|
||
|
|
|
||
|
|
:param function callback: Callback function to fire each time a button
|
||
|
|
state changes.
|
||
|
|
"""
|
||
|
|
self.touchscreen_callback = callback
|
||
|
|
|
||
|
|
def set_touchscreen_callback_async(self, async_callback, loop=None):
|
||
|
|
"""
|
||
|
|
Sets the asynchronous callback function called each time there is an
|
||
|
|
interaction with the touchscreen on the StreamDock. The given callback
|
||
|
|
should be compatible with Python 3's `asyncio` routines.
|
||
|
|
|
||
|
|
.. note:: The asynchronous callback will be fired in a thread-safe
|
||
|
|
manner.
|
||
|
|
|
||
|
|
.. note:: This will override the callback (if any) set by
|
||
|
|
:func:`~StreamDock.set_touchscreen_callback`.
|
||
|
|
|
||
|
|
:param function async_callback: Asynchronous callback function to fire
|
||
|
|
each time a button state changes.
|
||
|
|
:param asyncio.loop loop: Asyncio loop to dispatch the callback into
|
||
|
|
"""
|
||
|
|
import asyncio
|
||
|
|
|
||
|
|
loop = loop or asyncio.get_event_loop()
|
||
|
|
|
||
|
|
def callback(*args):
|
||
|
|
asyncio.run_coroutine_threadsafe(async_callback(*args), loop)
|
||
|
|
|
||
|
|
self.set_touchscreen_callback(callback)
|