From 06f4c2768c9c3b71c0cc314dde5c7910bbab2e24 Mon Sep 17 00:00:00 2001 From: Karl Date: Thu, 7 May 2026 19:14:07 +0100 Subject: [PATCH] feat: initial implementation of StreamDock Home Assistant integration This commit introduces a complete integration for Mirabox StreamDock devices with Home Assistant, allowing users to control entities and monitor states directly from the hardware. Key features included: - Support for multiple Mirabox models: N1, N3, N4, N4Pro, XL, M3, M18, K1Pro, and various 293 variants. - Home Assistant WebSocket integration for real-time entity updates and service execution. - Dynamic LCD key rendering with support for custom icons, labels, and entity-state aware colors. - Input handling for physical buttons and rotary encoders (knobs). - Multi-page navigation support with configurable cycle keys and knobs. - Cross-platform transport layer using a custom HID library. - Configuration system using YAML with page-based layouts. - Linux udev rules for non-root USB access. --- .gitignore | 10 + 99-streamdock.rules | 53 + README.md | 214 +++ StreamDock/DeviceManager.py | 249 +++ StreamDock/Devices/K1Pro.py | 225 +++ StreamDock/Devices/StreamDock.py | 600 +++++++ StreamDock/Devices/StreamDock293.py | 157 ++ StreamDock/Devices/StreamDock293V3.py | 168 ++ StreamDock/Devices/StreamDock293s.py | 167 ++ StreamDock/Devices/StreamDock293sV3.py | 179 +++ StreamDock/Devices/StreamDockM18.py | 179 +++ StreamDock/Devices/StreamDockM3.py | 230 +++ StreamDock/Devices/StreamDockN1.py | 323 ++++ StreamDock/Devices/StreamDockN3.py | 142 ++ StreamDock/Devices/StreamDockN4.py | 210 +++ StreamDock/Devices/StreamDockN4Pro.py | 306 ++++ StreamDock/Devices/StreamDockXL.py | 242 +++ StreamDock/Devices/__init__.py | 0 StreamDock/FeatrueOption.py | 24 + StreamDock/ImageHelpers/PILHelper.py | 89 ++ StreamDock/ImageHelpers/__init__.py | 0 StreamDock/InputTypes.py | 109 ++ StreamDock/ProductIDs.py | 129 ++ StreamDock/Transport/LibUSBHIDAPI.py | 1398 +++++++++++++++++ .../Transport/TransportDLL/libtransport.so | Bin 0 -> 207736 bytes .../TransportDLL/libtransport_arm64.dylib | Bin 0 -> 210728 bytes .../TransportDLL/libtransport_arm64.so | Bin 0 -> 187688 bytes StreamDock/Transport/__init__.py | 0 StreamDock/__init__.py | 0 config.example.yaml | 123 ++ config.py | 30 + key_renderer.py | 308 ++++ streamdock_ha.py | 423 +++++ 33 files changed, 6287 insertions(+) create mode 100644 .gitignore create mode 100644 99-streamdock.rules create mode 100644 README.md create mode 100644 StreamDock/DeviceManager.py create mode 100644 StreamDock/Devices/K1Pro.py create mode 100644 StreamDock/Devices/StreamDock.py create mode 100644 StreamDock/Devices/StreamDock293.py create mode 100644 StreamDock/Devices/StreamDock293V3.py create mode 100644 StreamDock/Devices/StreamDock293s.py create mode 100644 StreamDock/Devices/StreamDock293sV3.py create mode 100644 StreamDock/Devices/StreamDockM18.py create mode 100644 StreamDock/Devices/StreamDockM3.py create mode 100644 StreamDock/Devices/StreamDockN1.py create mode 100644 StreamDock/Devices/StreamDockN3.py create mode 100644 StreamDock/Devices/StreamDockN4.py create mode 100644 StreamDock/Devices/StreamDockN4Pro.py create mode 100644 StreamDock/Devices/StreamDockXL.py create mode 100644 StreamDock/Devices/__init__.py create mode 100644 StreamDock/FeatrueOption.py create mode 100644 StreamDock/ImageHelpers/PILHelper.py create mode 100644 StreamDock/ImageHelpers/__init__.py create mode 100644 StreamDock/InputTypes.py create mode 100644 StreamDock/ProductIDs.py create mode 100644 StreamDock/Transport/LibUSBHIDAPI.py create mode 100644 StreamDock/Transport/TransportDLL/libtransport.so create mode 100644 StreamDock/Transport/TransportDLL/libtransport_arm64.dylib create mode 100644 StreamDock/Transport/TransportDLL/libtransport_arm64.so create mode 100644 StreamDock/Transport/__init__.py create mode 100644 StreamDock/__init__.py create mode 100644 config.example.yaml create mode 100644 config.py create mode 100644 key_renderer.py create mode 100644 streamdock_ha.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5028fe6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +venv/ +__pycache__/ +*.pyc +*.pyo +.DS_Store +*.egg-info/ +dist/ +build/ +rotated_*.jpg +config.yaml \ No newline at end of file diff --git a/99-streamdock.rules b/99-streamdock.rules new file mode 100644 index 0000000..7d545a2 --- /dev/null +++ b/99-streamdock.rules @@ -0,0 +1,53 @@ +# StreamDock USB device permission configuration +# This file sets appropriate USB permissions for StreamDock devices, allowing ordinary users to access + +# StreamDock 293 series +SUBSYSTEM=="usb", ATTR{idVendor}=="5500", ATTR{idProduct}=="1001", MODE="0666", GROUP="plugdev" +SUBSYSTEM=="usb", ATTR{idVendor}=="5548", ATTR{idProduct}=="6670", MODE="0666", GROUP="plugdev" +SUBSYSTEM=="usb", ATTR{idVendor}=="6603", ATTR{idProduct}=="1005", MODE="0666", GROUP="plugdev" +SUBSYSTEM=="usb", ATTR{idVendor}=="6603", ATTR{idProduct}=="1006", MODE="0666", GROUP="plugdev" +SUBSYSTEM=="usb", ATTR{idVendor}=="6603", ATTR{idProduct}=="1010", MODE="0666", GROUP="plugdev" +SUBSYSTEM=="usb", ATTR{idVendor}=="6603", ATTR{idProduct}=="1014", MODE="0666", GROUP="plugdev" + +# StreamDock N3 series +SUBSYSTEM=="usb", ATTR{idVendor}=="6603", ATTR{idProduct}=="1002", MODE="0666", GROUP="plugdev" +SUBSYSTEM=="usb", ATTR{idVendor}=="6603", ATTR{idProduct}=="1003", MODE="0666", GROUP="plugdev" +SUBSYSTEM=="usb", ATTR{idVendor}=="6602", ATTR{idProduct}=="1000", MODE="0666", GROUP="plugdev" +SUBSYSTEM=="usb", ATTR{idVendor}=="6602", ATTR{idProduct}=="1002", MODE="0666", GROUP="plugdev" +SUBSYSTEM=="usb", ATTR{idVendor}=="6602", ATTR{idProduct}=="1003", MODE="0666", GROUP="plugdev" +SUBSYSTEM=="usb", ATTR{idVendor}=="6602", ATTR{idProduct}=="2929", MODE="0666", GROUP="plugdev" +SUBSYSTEM=="usb", ATTR{idVendor}=="1500", ATTR{idProduct}=="3001", MODE="0666", GROUP="plugdev" + +# StreamDock N4 series +SUBSYSTEM=="usb", ATTR{idVendor}=="6602", ATTR{idProduct}=="1001", MODE="0666", GROUP="plugdev" +SUBSYSTEM=="usb", ATTR{idVendor}=="6603", ATTR{idProduct}=="1007", MODE="0666", GROUP="plugdev" + +# StreamDock N1 series +SUBSYSTEM=="usb", ATTR{idVendor}=="6603", ATTR{idProduct}=="1011", MODE="0666", GROUP="plugdev" +SUBSYSTEM=="usb", ATTR{idVendor}=="6603", ATTR{idProduct}=="1000", MODE="0666", GROUP="plugdev" + +# StreamDock N4Pro series +SUBSYSTEM=="usb", ATTR{idVendor}=="5548", ATTR{idProduct}=="1008", MODE="0666", GROUP="plugdev" +SUBSYSTEM=="usb", ATTR{idVendor}=="5548", ATTR{idProduct}=="1021", MODE="0666", GROUP="plugdev" + +# StreamDock XL series +SUBSYSTEM=="usb", ATTR{idVendor}=="5548", ATTR{idProduct}=="1028", MODE="0666", GROUP="plugdev" +SUBSYSTEM=="usb", ATTR{idVendor}=="5548", ATTR{idProduct}=="1031", MODE="0666", GROUP="plugdev" + +# StreamDock M18 series +SUBSYSTEM=="usb", ATTR{idVendor}=="6603", ATTR{idProduct}=="1009", MODE="0666", GROUP="plugdev" +SUBSYSTEM=="usb", ATTR{idVendor}=="6603", ATTR{idProduct}=="1012", MODE="0666", GROUP="plugdev" + +# StreamDock M3 series +SUBSYSTEM=="usb", ATTR{idVendor}=="5548", ATTR{idProduct}=="1020", MODE="0666", GROUP="plugdev" + +# StreamDock K1Pro series +SUBSYSTEM=="usb", ATTR{idVendor}=="6603", ATTR{idProduct}=="1015", MODE="0666", GROUP="plugdev" +SUBSYSTEM=="usb", ATTR{idVendor}=="6603", ATTR{idProduct}=="1019", MODE="0666", GROUP="plugdev" + +# HID device permission configuration (for HID interface access) +KERNEL=="hidraw*", ATTRS{idVendor}=="5500", MODE="0666", GROUP="plugdev" +KERNEL=="hidraw*", ATTRS{idVendor}=="5548", MODE="0666", GROUP="plugdev" +KERNEL=="hidraw*", ATTRS{idVendor}=="6603", MODE="0666", GROUP="plugdev" +KERNEL=="hidraw*", ATTRS{idVendor}=="6602", MODE="0666", GROUP="plugdev" +KERNEL=="hidraw*", ATTRS{idVendor}=="1500", MODE="0666", GROUP="plugdev" \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..4aae48d --- /dev/null +++ b/README.md @@ -0,0 +1,214 @@ +# StreamDock Home Assistant Integration + +Displays Home Assistant entity states on Mirabox StreamDock LCD keys and triggers HA services on button/knob presses. + +Tested on **StreamDock N3** (6 LCD keys + 3 knobs). Should work with other Mirabox StreamDock models (N1, N4, N4Pro, M3, XL, 293 variants, K1Pro). + +## Requirements + +- Python 3.11+ +- A Mirabox StreamDock device +- Home Assistant with a long-lived access token +- Linux (x86_64 or aarch64), macOS, or Windows + +### System packages (Debian/Raspberry Pi OS) + +```bash +sudo apt install python3-venv fonts-dejavu-core +``` + +For USB device permissions (avoid running as root): + +```bash +sudo cp 99-streamdock.rules /etc/udev/rules.d/ +sudo udevadm control --reload-rules +sudo udevadm trigger +# Then unplug and re-plug the StreamDock +``` + +### Python packages + +```bash +python3 -m venv venv +source venv/bin/activate +pip install Pillow pyyaml HomeAssistant-API pyudev +``` + +## Setup + +1. Create your config from the example: + +```bash +cp config.example.yaml config.yaml +``` + +2. Edit `config.yaml` with your HA connection details: + +```yaml +homeassistant: + url: "ws://YOUR_HA_IP:8123/api/websocket" + token: "YOUR_LONG_LIVED_ACCESS_TOKEN" +``` + +Get a token from Home Assistant: **Settings → People → Your User → Security → Long-Lived Access Tokens** + +3. Run: + +```bash +source venv/bin/activate +python3 streamdock_ha.py +``` + +## Configuration Reference + +Full `config.yaml` structure: + +```yaml +homeassistant: + url: "ws://homeassistant.local:8123/api/websocket" + token: "YOUR_TOKEN" + +navigation: + page_cycle: "button_8" # which button cycles pages (middle of 3 bottom buttons) + knob_left: "knob_1" # knob whose left-rotation goes to previous page + knob_right: "knob_2" # knob whose right-rotation goes to next page + knob_press_left: "knob_1" # knob whose press goes to previous page + knob_press_right: "knob_2"# knob whose press goes to next page + +pages: + - name: "Lights" + keys: + 1: # LCD key number (1-6 on N3) + entity: "light.living_room" # HA entity ID + service: "toggle" # HA service to call on press + icon: "lightbulb" # icon name (see below) + label: "Living" # short label shown on key + 2: + entity: "switch.kitchen" + service: "toggle" + icon: "power" + label: "Kitchen" + 3: # sensor with no service = display-only + entity: "sensor.temperature" + icon: "thermometer" + label: "Temp" + knobs: # per-page knob bindings + knob_3: + rotate_left: + entity: "script.volume_down" + service: "turn_on" + rotate_right: + entity: "script.volume_up" + service: "turn_on" + press: + entity: "script.mute" + service: "turn_on" + + - name: "Climate" + keys: + 1: + entity: "climate.bedroom" + service: "toggle" + icon: "thermostat" + label: "Bedroom" +``` + +### Key Numbers + +| Keys | N3 | N1 | N4/N4Pro | XL | +|------|----|----|-----------|-----| +| LCD keys | 1-6 | 1-15 | 6-15 | 1-32 | + +Bottom buttons (no LCD): keys 7, 8, 9 (N3). Key 8 is the middle one. + +### Knob Names + +| Knob | N3 position | +|------|------------| +| `knob_1` | Bottom-left | +| `knob_2` | Bottom-right | +| `knob_3` | Top | + +### Navigation Button/ Knob References + +Buttons can be referenced as `button_7`, `button_8`, `button_9`, `key_7`, etc., or just the number `7`, `8`, `9`. + +Knobs: `knob_1`, `knob_2`, `knob_3`, `knob_4`, or aliases `left`, `right`, `top`, `bottom_left`, `bottom_right`. + +### Available Icons + +`lightbulb`, `power`, `thermometer`, `thermostat`, `play`, `film`, `door`, `lock`, `speaker`, `fan`, or `default` + +Icons render with distinct "on" and "off" colors. For example, `lightbulb` shows yellow when on, gray when off. `door` shows green when open, red when closed. + +### Entity State Detection + +The app automatically detects on/off states from HA entity states: + +- **On**: `on`, `playing`, `open`, `unlocked`, `home`, `heat`, `cool`, `auto` +- **Off**: everything else + +Sensor values (temperature, humidity, etc.) are displayed as-is on the key. + +## Running on Raspberry Pi + +The included `libtransport_arm64.so` supports aarch64 Linux (Pi 3, 4, 5, Zero 2 W). The library loader auto-detects the architecture. + +```bash +# On Raspberry Pi OS +sudo apt install python3-venv fonts-dejavu-core +python3 -m venv venv +source venv/bin/activate +pip install Pillow pyyaml HomeAssistant-API pyudev + +# USB permissions +sudo cp 99-streamdock.rules /etc/udev/rules.d/ +sudo udevadm control --reload-rules && sudo udevadm trigger + +# Configure and run +cp config.example.yaml config.yaml +nano config.yaml +python3 streamdock_ha.py +``` + +### Autostart on Boot (systemd) + +Create `/etc/systemd/system/streamdock-ha.service`: + +```ini +[Unit] +Description=StreamDock Home Assistant +After=network.target + +[Service] +Type=simple +User=karl +WorkingDirectory=/home/karl/streamdocklinux +ExecStart=/home/karl/streamdocklinux/venv/bin/python3 streamdock_ha.py +Restart=on-failure +RestartSec=10 + +[Install] +WantedBy=multi-user.target +``` + +```bash +sudo systemctl enable streamdock-ha +sudo systemctl start streamdock-ha +``` + +## Architecture + +``` +streamdock_ha.py - Main app: HA WebSocket + StreamDock event loop +key_renderer.py - Dynamic key image generation (icons + state text) +config.py - YAML config loader +config.yaml - User configuration (gitignored) +config.example.yaml - Example configuration +99-streamdock.rules - udev rules for USB permissions +StreamDock/ - Mirabox StreamDock Device SDK (Python + C transport lib) +``` + +## License + +This project includes the Mirabox StreamDock Device SDK. See `StreamDock/` for its license terms. \ No newline at end of file diff --git a/StreamDock/DeviceManager.py b/StreamDock/DeviceManager.py new file mode 100644 index 0000000..80bdbf8 --- /dev/null +++ b/StreamDock/DeviceManager.py @@ -0,0 +1,249 @@ +import platform +import threading +import time +from typing import Optional +# import pywinusb.hid as hid +from .ProductIDs import USBVendorIDs, USBProductIDs, g_products +from .Transport.LibUSBHIDAPI import LibUSBHIDAPI + +# Platform-specific imports +if platform.system() == "Linux": + import pyudev +elif platform.system() == "Windows": + try: + import wmi + import pythoncom + WINDOWS_SUPPORT = True + except ImportError: + print("Warning: wmi module not installed, using polling mode") + WINDOWS_SUPPORT = False +elif platform.system() == "Darwin": + # macOS specific imports can be added here if needed + pass + +class DeviceManager: + streamdocks = list() + + @staticmethod + def _get_transport(transport): + return LibUSBHIDAPI() + + def __init__(self, transport=None): + self.transport = self._get_transport(transport) + + def enumerate(self)->list: + # CRITICAL: Clear old list to avoid stale references + self.streamdocks.clear() + + products = g_products + for vid, pid, class_type in products: + found_devices = self.transport.enumerate_devices(vendor_id = vid, product_id = pid) + # Create a dedicated LibUSBHIDAPI instance per device + # CRITICAL: Pass device info to transport for proper resource management + for d in found_devices: + # Create device_info structure from dict + device_info = LibUSBHIDAPI.create_device_info_from_dict(d) + device_transport = LibUSBHIDAPI(device_info) + self.streamdocks.append(class_type(device_transport, d)) + return self.streamdocks + + def listen(self): + """ + Listen for device hotplug events, cross-platform + """ + products = g_products + system = platform.system() + + if system == "Linux": + self._listen_linux(products) + elif system == "Windows": + self._listen_windows(products) + elif system == "Darwin": + self._listen_macos(products) + else: + print(f"Unsupported operating system: {system}") + + def _listen_linux(self, products): + """Linux uses pyudev to listen for device events""" + context = pyudev.Context() + monitor = pyudev.Monitor.from_netlink(context) + monitor.filter_by(subsystem='usb') + + for device in iter(monitor.poll, None): + self._handle_device_event(device.action, device, products) + + def _listen_windows(self, products): + """Windows uses WMI to listen for device events""" + if not WINDOWS_SUPPORT: + print("WMI unavailable, using polling mode") + self._fallback_polling(products) + return + + try: + pythoncom.CoInitialize() + c = wmi.WMI() + + # Listen for device connection events + watcher = c.Win32_DeviceChangeEvent.watch_for( + EventType=2 # Device connected + ) + + while True: + try: + event = watcher() + if event.EventType == 2: # Device connected + self._check_new_devices_windows(products) + elif event.EventType == 3: # Device disconnected + self._check_removed_devices_windows(products) + except Exception as e: + print(f"Windows device listener error: {e}") + time.sleep(1) + except Exception as e: + print(f"Windows WMI initialization failed: {e}") + # Fall back to polling mode + self._fallback_polling(products) + finally: + pythoncom.CoUninitialize() + + def _listen_macos(self, products): + """macOS uses polling to listen for device events""" + self._fallback_polling(products) + + def _fallback_polling(self, products): + """Fall back to polling mode for systems without real-time monitoring""" + # print("Using polling mode to monitor device changes...") + current_devices = set() + + # Initialize current device list + for vid, pid, _ in products: + devices = self.transport.enumerate_devices(vendor_id=vid, product_id=pid) + for device in devices: + current_devices.add(device['path']) + + while True: + try: + new_devices = set() + for vid, pid, _ in products: + devices = self.transport.enumerate_devices(vendor_id=vid, product_id=pid) + for device in devices: + new_devices.add(device['path']) + + # Check for newly added devices + added_devices = new_devices - current_devices + for device_path in added_devices: + print(f"[add] path: {device_path}") + self._handle_device_addition(device_path, products) + + # Check for removed devices + removed_devices = current_devices - new_devices + for device_path in removed_devices: + print(f"[remove] path: {device_path}") + self._handle_device_removal(device_path) + + current_devices = new_devices + time.sleep(2) # Check every 2 seconds + except Exception as e: + print(f"Polling listener error: {e}") + time.sleep(5) + + def _handle_device_event(self, action, device, products): + """Handle device events (Linux)""" + if action not in ['add', 'remove']: + return + + if action == 'remove': + for willRemoveDevice in self.streamdocks: + if device.device_path.find(willRemoveDevice.getPath()) != -1: + print("[remove] path: " + willRemoveDevice.getPath()) + self.streamdocks.remove(willRemoveDevice) + break + + vendor_id_str = device.get('ID_VENDOR_ID') + product_id_str = device.get('ID_MODEL_ID') + + if not vendor_id_str or not product_id_str: + return + + try: + vendor_id = int(vendor_id_str, 16) + product_id = int(product_id_str, 16) + except ValueError: + return + + for vid, pid, class_type in products: + if vendor_id == vid and product_id == pid: + if action == 'add': + dev_path = device.device_path.split('/')[-1] + ":1.0" + full_path = dev_path + + found_devices = self.transport.enumerate_devices(vendor_id=vid, product_id=pid) + for d in found_devices: + if d['path'].endswith(full_path): + print("[add] path:", d['path']) + newDevice = class_type(self.transport, d) + self.streamdocks.append(newDevice) + newDevice.open() + # your reconnect logic like the next two line + # newDevice.set_key_image(1, "../img/tiga64.png") + # newDevice.refresh() + break + + def _check_new_devices_windows(self, products): + """Check for new devices on Windows""" + for vid, pid, class_type in products: + found_devices = self.transport.enumerate_devices(vendor_id=vid, product_id=pid) + for device_info in found_devices: + device_path = device_info['path'] + # Check whether the device already exists + exists = any(device.getPath() == device_path for device in self.streamdocks) + if not exists: + print(f"[add] path: {device_path}") + newDevice = class_type(self.transport, device_info) + self.streamdocks.append(newDevice) + newDevice.open() + # your reconnect logic like the next two line + # newDevice.set_key_image(1, "../img/tiga64.png") + # newDevice.refresh() + + def _check_removed_devices_windows(self, products): + """Check for removed devices on Windows""" + current_paths = set() + for vid, pid, _ in products: + found_devices = self.transport.enumerate_devices(vendor_id=vid, product_id=pid) + for device_info in found_devices: + current_paths.add(device_info['path']) + + # Remove devices that no longer exist + devices_to_remove = [] + for device in self.streamdocks: + if device.getPath() not in current_paths: + devices_to_remove.append(device) + + for device in devices_to_remove: + print(f"[remove] path: {device.getPath()}") + self.streamdocks.remove(device) + + def _handle_device_addition(self, device_path, products): + """Handle device addition events (polling mode)""" + for vid, pid, class_type in products: + found_devices = self.transport.enumerate_devices(vendor_id=vid, product_id=pid) + for device_info in found_devices: + if device_info['path'] == device_path: + newDevice = class_type(self.transport, device_info) + self.streamdocks.append(newDevice) + newDevice.open() + # your reconnect logic like the next two line + # newDevice.set_key_image(1, "../img/tiga64.png") + # newDevice.refresh() + break + + def _handle_device_removal(self, device_path): + """Handle device removal events (polling mode)""" + devices_to_remove = [] + for device in self.streamdocks: + if device.getPath() == device_path: + devices_to_remove.append(device) + + for device in devices_to_remove: + self.streamdocks.remove(device) + diff --git a/StreamDock/Devices/K1Pro.py b/StreamDock/Devices/K1Pro.py new file mode 100644 index 0000000..0eb6345 --- /dev/null +++ b/StreamDock/Devices/K1Pro.py @@ -0,0 +1,225 @@ +from StreamDock.FeatrueOption import device_type +from .StreamDock import StreamDock +from ..InputTypes import InputEvent, ButtonKey, EventType, KnobId, Direction +from PIL import Image +import ctypes +import ctypes.util +import os, io +from ..ImageHelpers.PILHelper import * +import random + + +class K1Pro(StreamDock): + """K1Pro device class - supports 6 keys and 3 knobs""" + + KEY_COUNT = 6 + KEY_MAP = False + + # Image key mapping: logical key -> hardware key (for setting images) + _IMAGE_KEY_MAP = { + ButtonKey.KEY_1: 0x05, + ButtonKey.KEY_2: 0x03, + ButtonKey.KEY_3: 0x01, + ButtonKey.KEY_4: 0x06, + ButtonKey.KEY_5: 0x04, + ButtonKey.KEY_6: 0x02, + } + + # Reverse mapping: hardware key -> logical key (for event decoding) + _HW_TO_LOGICAL_KEY = {v: k for k, v in _IMAGE_KEY_MAP.items()} + + def __init__(self, transport1, devInfo): + super().__init__(transport1, devInfo) + + 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 + """ + if logical_key in self._IMAGE_KEY_MAP: + return self._IMAGE_KEY_MAP[logical_key] + raise ValueError(f"K1Pro: Unsupported key {logical_key}") + + def decode_input_event(self, hardware_code: int, state: int) -> InputEvent: + """ + Decode hardware event codes into a unified InputEvent + + Hardware code mapping: + - Keys: 0x05, 0x03, 0x01, 0x06, 0x04, 0x02 + - Knob 1 press: 0x25 + - Knob 2 press: 0x30 + - Knob 3 press: 0x31 + - Knob 1 rotation: 0x50 (left), 0x51 (right) + - Knob 2 rotation: 0x60 (left), 0x61 (right) + - Knob 3 rotation: 0x90 (left), 0x91 (right) + """ + # Handle state value: 0x02=release, 0x01=press + normalized_state = 1 if state == 0x01 else 0 + + # Regular button events + if hardware_code in self._HW_TO_LOGICAL_KEY: + return InputEvent( + event_type=EventType.BUTTON, + key=self._HW_TO_LOGICAL_KEY[hardware_code], + state=normalized_state, + ) + + # Knob press event + knob_press_map = { + 0x25: KnobId.KNOB_1, + 0x30: KnobId.KNOB_2, + 0x31: KnobId.KNOB_3, + } + if hardware_code in knob_press_map: + return InputEvent( + event_type=EventType.KNOB_PRESS, + knob_id=knob_press_map[hardware_code], + state=normalized_state, + ) + + # Knob rotation event + knob_rotate_map = { + 0x50: (KnobId.KNOB_1, Direction.LEFT), + 0x51: (KnobId.KNOB_1, Direction.RIGHT), + 0x60: (KnobId.KNOB_2, Direction.LEFT), + 0x61: (KnobId.KNOB_2, Direction.RIGHT), + 0x90: (KnobId.KNOB_3, Direction.LEFT), + 0x91: (KnobId.KNOB_3, Direction.RIGHT), + } + if hardware_code in knob_rotate_map: + knob_id, direction = knob_rotate_map[hardware_code] + return InputEvent( + event_type=EventType.KNOB_ROTATE, knob_id=knob_id, direction=direction + ) + + # Unknown event + return InputEvent(event_type=EventType.UNKNOWN) + + # Set device screen brightness + def set_brightness(self, percent): + return self.transport.setBrightness(percent) + + def set_touchscreen_image(self, path): + """Background setting not supported""" + return 0 + + # Set device key icon image 64 * 64 + def set_key_image(self, key, path): + try: + if isinstance(key, int): + if key not in range(1, 7): + print(f"key '{key}' out of range. you should set (1 ~ 6)") + return -1 + logical_key = ButtonKey(key) + else: + logical_key = key + + if not os.path.exists(path): + print(f"Error: The image file '{path}' does not exist.") + return -1 + + # Get hardware key value + hardware_key = self.get_image_key(logical_key) + + # open formatter + image = Image.open(path) + image = to_native_key_format(self, image) + temp_image_path = ( + "rotated_key_image_" + str(random.randint(9999, 999999)) + ".jpg" + ) + image.save(temp_image_path) + + # encode send + path_bytes = temp_image_path.encode("utf-8") + c_path = ctypes.c_char_p(path_bytes) + res = self.transport.setKeyImgDualDevice(c_path, hardware_key) + os.remove(temp_image_path) + return res + + except Exception as e: + print(f"Error: {e}") + return -1 + + # TODO + def set_key_imageData(self, key, path): + pass + + # Get device serial number + def get_serial_number(self): + return self.serial_number + + def key_image_format(self): + return { + "size": (64, 64), + "format": "JPEG", + "rotation": -90, + "flip": (False, False), + } + + def touchscreen_image_format(self): + return { + "size": (800, 480), + "format": "JPEG", + "rotation": 180, + "flip": (False, False), + } + + # Set device parameters + def set_device(self): + self.transport.set_report_size(513, 1025, 0) + self.transport.set_report_id(0x04) + self.feature_option.deviceType = device_type.k1pro + pass + + def set_keyboard_backlight_brightness(self, brightness): + """ + Set the keyboard backlight brightness. + + Args: + brightness: Brightness value (0-6) + """ + self.transport.set_keyboard_backlight_brightness(brightness) + + def set_keyboard_lighting_effects(self, effect: int): + """ + Set the keyboard lighting effect. + 0 is static lighting. + Args: + effect: Effect mode identifier (0-9) + """ + if(effect==0): + self.set_keyboard_lighting_speed(0) + self.transport.set_keyboard_lighting_effects(effect) + + def set_keyboard_lighting_speed(self, speed: int): + """ + Set the keyboard lighting effect speed. + Args: + speed: Speed value for lighting effects (0-7) + """ + self.transport.set_keyboard_lighting_speed(speed) + + def set_keyboard_rgb_backlight(self, red: int, green: int, blue: int): + """ + Set the keyboard RGB backlight color. + + Args: + red: Red component (0-255) + green: Green component (0-255) + blue: Blue component (0-255) + """ + self.transport.set_keyboard_rgb_backlight(red, green, blue) + + def keyboard_os_mode_switch(self, os_mode: int): + """ + Set the keyboard OS mode. + + Args: + os_mode: OS mode identifier + """ + self.transport.keyboard_os_mode_switch(os_mode) diff --git a/StreamDock/Devices/StreamDock.py b/StreamDock/Devices/StreamDock.py new file mode 100644 index 0000000..623a1d6 --- /dev/null +++ b/StreamDock/Devices/StreamDock.py @@ -0,0 +1,600 @@ +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) diff --git a/StreamDock/Devices/StreamDock293.py b/StreamDock/Devices/StreamDock293.py new file mode 100644 index 0000000..581e2e2 --- /dev/null +++ b/StreamDock/Devices/StreamDock293.py @@ -0,0 +1,157 @@ +from StreamDock.FeatrueOption import device_type +from .StreamDock import StreamDock +from ..InputTypes import InputEvent, ButtonKey, EventType +from PIL import Image +import ctypes +import ctypes.util +import os, io +from ..ImageHelpers.PILHelper import * +import random + + +class StreamDock293(StreamDock): + """StreamDock293 device class - supports 15 keys""" + + KEY_COUNT = 15 + KEY_MAP = False + + # Image key mapping: logical key -> hardware key (for setting images) + # The 293 device uses the base class KEY_MAPPING mapping + _IMAGE_KEY_MAP = { + ButtonKey.KEY_1: 11, + ButtonKey.KEY_2: 12, + ButtonKey.KEY_3: 13, + ButtonKey.KEY_4: 14, + ButtonKey.KEY_5: 15, + ButtonKey.KEY_6: 6, + ButtonKey.KEY_7: 7, + ButtonKey.KEY_8: 8, + ButtonKey.KEY_9: 9, + ButtonKey.KEY_10: 10, + ButtonKey.KEY_11: 1, + ButtonKey.KEY_12: 2, + ButtonKey.KEY_13: 3, + ButtonKey.KEY_14: 4, + ButtonKey.KEY_15: 5, + } + + # Reverse mapping: hardware key -> logical key (for event decoding) + _HW_TO_LOGICAL_KEY = {v: k for k, v in _IMAGE_KEY_MAP.items()} + + def __init__(self, transport1, devInfo): + super().__init__(transport1, devInfo) + + 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 + """ + if logical_key in self._IMAGE_KEY_MAP: + return self._IMAGE_KEY_MAP[logical_key] + raise ValueError(f"StreamDock293: Unsupported key {logical_key}") + + def decode_input_event(self, hardware_code: int, state: int) -> InputEvent: + """ + Decode hardware event codes into a unified InputEvent + + The 293 device supports only regular buttons; hardware code range 1-15 + """ + # Handle state value: 0x02=release, 0x01=press + normalized_state = 1 if state == 0x01 else 0 + + # Regular button events (1-15) + if hardware_code in self._HW_TO_LOGICAL_KEY: + return InputEvent( + event_type=EventType.BUTTON, + key=self._HW_TO_LOGICAL_KEY[hardware_code], + state=normalized_state + ) + + # Unknown event + return InputEvent(event_type=EventType.UNKNOWN) + + # Set device screen brightness + def set_brightness(self, percent): + return self.transport.setBrightness(percent) + + # Set device background image 800 * 480 + def set_touchscreen_image(self, path): + try: + if not os.path.exists(path): + print(f"Error: The image file '{path}' does not exist.") + return -1 + image = Image.open(path) + image = to_native_touchscreen_format(self, image) + width, height = image.size + bgr_data = [] + for y in range(height): + for x in range(width): + r,g,b = image.getpixel((x,y)) + bgr_data.extend([b,g,r]) + arr_type = ctypes.c_char * len(bgr_data) + arr_ctypes = arr_type(*bgr_data) + return self.transport.setBackgroundImg(ctypes.cast(arr_ctypes, ctypes.POINTER(ctypes.c_ubyte)),width * height * 3) + + except Exception as e: + print(f"Error: {e}") + return -1 + + # Set device key icon image 100 * 100 + def set_key_image(self, key, path): + try: + if isinstance(key, int): + if key not in range(1, 16): + print(f"key '{key}' out of range. you should set (1 ~ 15)") + return -1 + logical_key = ButtonKey(key) + else: + logical_key = key + + if not os.path.exists(path): + print(f"Error: The image file '{path}' does not exist.") + return -1 + + # Get hardware key value + hardware_key = self.get_image_key(logical_key) + + image = Image.open(path) + rotated_image = to_native_key_format(self, image) + rotated_image.save("Temporary.jpg", "JPEG", subsampling=0, quality=100) + returnvalue = self.transport.setKeyImg(bytes("Temporary.jpg",'utf-8'), hardware_key) + os.remove("Temporary.jpg") + return returnvalue + + except Exception as e: + print(f"Error: {e}") + return -1 + + # Get device firmware version + def get_serial_number(self): + return self.serial_number + + def key_image_format(self): + return { + 'size': (100, 100), + 'format': "JPEG", + 'rotation': 180, + 'flip': (False, False) + } + + def touchscreen_image_format(self): + return { + 'size': (800, 480), + 'format': "JPEG", + 'rotation': 180, + 'flip': (False, False) + } + + # Set device parameters + def set_device(self): + self.transport.set_report_size(513, 513, 0) + self.feature_option.deviceType = device_type.dock_293 + pass diff --git a/StreamDock/Devices/StreamDock293V3.py b/StreamDock/Devices/StreamDock293V3.py new file mode 100644 index 0000000..e05fbb3 --- /dev/null +++ b/StreamDock/Devices/StreamDock293V3.py @@ -0,0 +1,168 @@ +from StreamDock.FeatrueOption import device_type +from .StreamDock import StreamDock +from ..InputTypes import InputEvent, ButtonKey, EventType +from PIL import Image +import ctypes +import ctypes.util +import os, io +from ..ImageHelpers.PILHelper import * +import random + + +class StreamDock293V3(StreamDock): + """StreamDock293V3 device class - supports 15 keys""" + + KEY_COUNT = 15 + KEY_MAP = False + + # Image key mapping: logical key -> hardware key (for setting images) + _IMAGE_KEY_MAP = { + ButtonKey.KEY_1: 11, + ButtonKey.KEY_2: 12, + ButtonKey.KEY_3: 13, + ButtonKey.KEY_4: 14, + ButtonKey.KEY_5: 15, + ButtonKey.KEY_6: 6, + ButtonKey.KEY_7: 7, + ButtonKey.KEY_8: 8, + ButtonKey.KEY_9: 9, + ButtonKey.KEY_10: 10, + ButtonKey.KEY_11: 1, + ButtonKey.KEY_12: 2, + ButtonKey.KEY_13: 3, + ButtonKey.KEY_14: 4, + ButtonKey.KEY_15: 5, + } + + # Reverse mapping: hardware key -> logical key (for event decoding) + _HW_TO_LOGICAL_KEY = {v: k for k, v in _IMAGE_KEY_MAP.items()} + + def __init__(self, transport1, devInfo): + super().__init__(transport1, devInfo) + + 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 + """ + if logical_key in self._IMAGE_KEY_MAP: + return self._IMAGE_KEY_MAP[logical_key] + raise ValueError(f"StreamDock293V3: Unsupported key {logical_key}") + + def decode_input_event(self, hardware_code: int, state: int) -> InputEvent: + """ + Decode hardware event codes into a unified InputEvent + + The 293V3 device supports only regular buttons; hardware code range 1-15 + """ + # Handle state value: 0x02=release, 0x01=press + normalized_state = 1 if state == 0x01 else 0 + + # Regular button events (1-15) + if hardware_code in self._HW_TO_LOGICAL_KEY: + return InputEvent( + event_type=EventType.BUTTON, + key=self._HW_TO_LOGICAL_KEY[hardware_code], + state=normalized_state + ) + + # Unknown event + return InputEvent(event_type=EventType.UNKNOWN) + + # Set device screen brightness + def set_brightness(self, percent): + return self.transport.setBrightness(percent) + + # Set device background image 800 * 480 + def set_touchscreen_image(self, path): + try: + if not os.path.exists(path): + print(f"Error: The image file '{path}' does not exist.") + return -1 + + # open formatter + image = Image.open(path) + image = to_native_touchscreen_format(self, image) + temp_image_path = "rotated_touchscreen_image_" + str(random.randint(9999, 999999)) + ".jpg" + image.save(temp_image_path) + + # encode send + path_bytes = temp_image_path.encode('utf-8') + c_path = ctypes.c_char_p(path_bytes) + res = self.transport.setBackgroundImgDualDevice(c_path) + os.remove(temp_image_path) + return res + + except Exception as e: + print(f"Error: {e}") + return -1 + + # Set device key icon image 112 * 112 + def set_key_image(self, key, path): + try: + if isinstance(key, int): + if key not in range(1, 16): + print(f"key '{key}' out of range. you should set (1 ~ 15)") + return -1 + logical_key = ButtonKey(key) + else: + logical_key = key + + if not os.path.exists(path): + print(f"Error: The image file '{path}' does not exist.") + return -1 + + # Get hardware key value + hardware_key = self.get_image_key(logical_key) + + # open formatter + image = Image.open(path) + image = to_native_key_format(self, image) + temp_image_path = "rotated_key_image_" + str(random.randint(9999, 999999)) + ".jpg" + image.save(temp_image_path) + + # encode send + path_bytes = temp_image_path.encode('utf-8') + c_path = ctypes.c_char_p(path_bytes) + res = self.transport.setKeyImgDualDevice(c_path, hardware_key) + os.remove(temp_image_path) + return res + + except Exception as e: + print(f"Error: {e}") + return -1 + + # TODO + def set_key_imageData(self, key, path): + pass + + # Get device firmware version + def get_serial_number(self): + return self.serial_number + + def key_image_format(self): + return { + 'size': (112, 112), + 'format': "JPEG", + 'rotation': 180, + 'flip': (False, False) + } + + def touchscreen_image_format(self): + return { + 'size': (800, 480), + 'format': "JPEG", + 'rotation': 180, + 'flip': (False, False) + } + + # Set device parameters + def set_device(self): + self.transport.set_report_size(513, 1025, 0) + self.feature_option.deviceType = device_type.dock_293v3 + pass diff --git a/StreamDock/Devices/StreamDock293s.py b/StreamDock/Devices/StreamDock293s.py new file mode 100644 index 0000000..d83ff46 --- /dev/null +++ b/StreamDock/Devices/StreamDock293s.py @@ -0,0 +1,167 @@ +# -*- coding: utf-8 -*- +from StreamDock.FeatrueOption import device_type +from .StreamDock import StreamDock +from ..InputTypes import InputEvent, ButtonKey, EventType +from PIL import Image +import ctypes +import ctypes.util +import os, io +from ..ImageHelpers.PILHelper import * +import random + + +class StreamDock293s(StreamDock): + """StreamDock293s device class - supports 15 keys and 3 secondary screen keys""" + + KEY_COUNT = 18 + KEY_MAP = False + + # Image key mapping: logical key -> hardware key (for setting images) + _IMAGE_KEY_MAP = { + ButtonKey.KEY_1: 13, + ButtonKey.KEY_2: 10, + ButtonKey.KEY_3: 7, + ButtonKey.KEY_4: 4, + ButtonKey.KEY_5: 1, + ButtonKey.KEY_6: 14, + ButtonKey.KEY_7: 11, + ButtonKey.KEY_8: 8, + ButtonKey.KEY_9: 5, + ButtonKey.KEY_10: 2, + ButtonKey.KEY_11: 15, + ButtonKey.KEY_12: 12, + ButtonKey.KEY_13: 9, + ButtonKey.KEY_14: 6, + ButtonKey.KEY_15: 3, + # Secondary screen keys 16-18 + ButtonKey.KEY_16: 16, + ButtonKey.KEY_17: 17, + ButtonKey.KEY_18: 18, + } + + # Reverse mapping: hardware key -> logical key (for event decoding) + _HW_TO_LOGICAL_KEY = {v: k for k, v in _IMAGE_KEY_MAP.items()} + + def __init__(self, transport1, devInfo): + super().__init__(transport1, devInfo) + + 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 + """ + if logical_key in self._IMAGE_KEY_MAP: + return self._IMAGE_KEY_MAP[logical_key] + raise ValueError(f"StreamDock293s: Unsupported key {logical_key}") + + def decode_input_event(self, hardware_code: int, state: int) -> InputEvent: + """ + Decode hardware event codes into a unified InputEvent + + The 293s device supports only regular buttons; hardware code range 1-18 + """ + # Handle state value: 0x02=release, 0x01=press + normalized_state = 1 if state == 0x01 else 0 + + # Regular button events (1-18) + if hardware_code in self._HW_TO_LOGICAL_KEY: + return InputEvent( + event_type=EventType.BUTTON, + key=self._HW_TO_LOGICAL_KEY[hardware_code], + state=normalized_state + ) + + # Unknown event + return InputEvent(event_type=EventType.UNKNOWN) + + # Set device screen brightness + def set_brightness(self, percent): + return self.transport.setBrightness(percent) + + # Set device background image 854 * 480 + def set_touchscreen_image(self, image): + image = Image.open(image) + image = to_native_touchscreen_format(self, image) + width, height = image.size + bgr_data = [] + + for x in range(width): + for y in range(height): + r,g,b = image.getpixel((x,y)) + bgr_data.extend([b,g,r]) + arr_type = ctypes.c_char * len(bgr_data) + arr_ctypes = arr_type(*bgr_data) + + return self.transport.setBackgroundImg(ctypes.cast(arr_ctypes, ctypes.POINTER(ctypes.c_ubyte)),width * height * 3) + + # Set device key icon image 85 * 85 + def set_key_image(self, key, path): + try: + if isinstance(key, int): + if key not in range(1, 19): + print(f"key '{key}' out of range. you should set (1 ~ 18)") + return -1 + logical_key = ButtonKey(key) + else: + logical_key = key + + if not os.path.exists(path): + print(f"Error: The image file '{path}' does not exist.") + return -1 + + # Get hardware key value + hardware_key = self.get_image_key(logical_key) + + image = Image.open(path) + if hardware_key in range(1, 16): + # icon + rotated_image = to_native_key_format(self, image) + elif hardware_key in range(16, 19): + # second screen + rotated_image = to_native_seondscreen_format(self, image) + rotated_image.save("Temporary.jpg", "JPEG", subsampling=0, quality=100) + returnvalue = self.transport.setKeyImg(bytes("Temporary.jpg",'utf-8'), hardware_key) + os.remove("Temporary.jpg") + return returnvalue + + except Exception as e: + print(f"Error: {e}") + return -1 + + def get_serial_number(self): + return self.serial_number + + def key_image_format(self): + return { + 'size': (85, 85), + 'format': "JPEG", + 'rotation': 90, + 'flip': (False, False) + } + + def secondscreen_image_format(self): + return { + 'size': (80, 80), + 'format': "JPEG", + 'rotation': 90, + 'flip': (False, False) + } + + def touchscreen_image_format(self): + return { + 'size': (854, 480), + 'format': "JPEG", + 'rotation': 0, + 'flip': (True, False) + } + + # Set device parameters + def set_device(self): + self.transport.set_report_size(513, 513, 0) + self.feature_option.deviceType = device_type.dock_293s + pass diff --git a/StreamDock/Devices/StreamDock293sV3.py b/StreamDock/Devices/StreamDock293sV3.py new file mode 100644 index 0000000..1a9c9c8 --- /dev/null +++ b/StreamDock/Devices/StreamDock293sV3.py @@ -0,0 +1,179 @@ +# -*- coding: utf-8 -*- +from StreamDock.FeatrueOption import device_type +from .StreamDock import StreamDock +from ..InputTypes import InputEvent, ButtonKey, EventType +from PIL import Image +import ctypes +import ctypes.util +import os, io +from ..ImageHelpers.PILHelper import * +import random + + +class StreamDock293sV3(StreamDock): + """StreamDock293sV3 device class - supports 15 keys and 3 secondary screen keys""" + + KEY_COUNT = 18 + KEY_MAP = False + + # Image key mapping: logical key -> hardware key (for setting images) + _IMAGE_KEY_MAP = { + ButtonKey.KEY_1: 13, + ButtonKey.KEY_2: 10, + ButtonKey.KEY_3: 7, + ButtonKey.KEY_4: 4, + ButtonKey.KEY_5: 1, + ButtonKey.KEY_6: 14, + ButtonKey.KEY_7: 11, + ButtonKey.KEY_8: 8, + ButtonKey.KEY_9: 5, + ButtonKey.KEY_10: 2, + ButtonKey.KEY_11: 15, + ButtonKey.KEY_12: 12, + ButtonKey.KEY_13: 9, + ButtonKey.KEY_14: 6, + ButtonKey.KEY_15: 3, + # Secondary screen keys 16-18 + ButtonKey.KEY_16: 16, + ButtonKey.KEY_17: 17, + ButtonKey.KEY_18: 18, + } + + # Reverse mapping: hardware key -> logical key (for event decoding) + _HW_TO_LOGICAL_KEY = {v: k for k, v in _IMAGE_KEY_MAP.items()} + + def __init__(self, transport1, devInfo): + super().__init__(transport1, devInfo) + + 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 + """ + if logical_key in self._IMAGE_KEY_MAP: + return self._IMAGE_KEY_MAP[logical_key] + raise ValueError(f"StreamDock293sV3: Unsupported key {logical_key}") + + def decode_input_event(self, hardware_code: int, state: int) -> InputEvent: + """ + Decode hardware event codes into a unified InputEvent + + The 293sV3 device supports only regular buttons; hardware code range 1-18 + """ + # Handle state value: 0x02=release, 0x01=press + normalized_state = 1 if state == 0x01 else 0 + + # Regular button events (1-18) + if hardware_code in self._HW_TO_LOGICAL_KEY: + return InputEvent( + event_type=EventType.BUTTON, + key=self._HW_TO_LOGICAL_KEY[hardware_code], + state=normalized_state, + ) + + # Unknown event + return InputEvent(event_type=EventType.UNKNOWN) + + # Set device screen brightness + def set_brightness(self, percent): + return self.transport.setBrightness(percent) + + # Set device background image 854 * 480 + def set_touchscreen_image(self, path): + try: + if not os.path.exists(path): + print(f"Error: The image file '{path}' does not exist.") + return -1 + + image = Image.open(path) + image = to_native_touchscreen_format(self, image) + temp_image_path = ( + "rotated_touchscreen_image_" + + str(random.randint(9999, 999999)) + + ".jpg" + ) + image.save(temp_image_path) + + # encode send + path_bytes = temp_image_path.encode("utf-8") + c_path = ctypes.c_char_p(path_bytes) + res = self.transport.setBackgroundImgDualDevice(c_path) + os.remove(temp_image_path) + return res + except Exception as e: + print(f"Error: {e}") + return -1 + + # Set device key icon image 85 * 85 + def set_key_image(self, key, path): + try: + if isinstance(key, int): + if key not in range(1, 19): + print(f"key '{key}' out of range. you should set (1 ~ 18)") + return -1 + logical_key = ButtonKey(key) + else: + logical_key = key + + if not os.path.exists(path): + print(f"Error: The image file '{path}' does not exist.") + return -1 + + # Get hardware key value + hardware_key = self.get_image_key(logical_key) + + image = Image.open(path) + if hardware_key in range(1, 16): + # icon + image = to_native_key_format(self, image) + elif hardware_key in range(16, 19): + # second screen + image = to_native_seondscreen_format(self, image) + image.save("Temporary.jpg", "JPEG", subsampling=0, quality=100) + returnvalue = self.transport.setKeyImg( + bytes("Temporary.jpg", "utf-8"), hardware_key + ) + os.remove("Temporary.jpg") + return returnvalue + + except Exception as e: + print(f"Error: {e}") + return -1 + + def get_serial_number(self): + return self.serial_number + + def key_image_format(self): + return { + "size": (96, 96), + "format": "JPEG", + "rotation": 90, + "flip": (False, False), + } + + def secondscreen_image_format(self): + return { + "size": (80, 80), + "format": "JPEG", + "rotation": 90, + "flip": (False, False), + } + + def touchscreen_image_format(self): + return { + "size": (854, 480), + "format": "JPEG", + "rotation": 90, + "flip": (False, False), + } + + # Set device parameters + def set_device(self): + self.transport.set_report_size(513, 1025, 0) + self.feature_option.deviceType = device_type.dock_293sv3 + pass diff --git a/StreamDock/Devices/StreamDockM18.py b/StreamDock/Devices/StreamDockM18.py new file mode 100644 index 0000000..e419558 --- /dev/null +++ b/StreamDock/Devices/StreamDockM18.py @@ -0,0 +1,179 @@ +from StreamDock.FeatrueOption import device_type +from .StreamDock import StreamDock +from ..InputTypes import InputEvent, ButtonKey, EventType +from PIL import Image +import ctypes +import ctypes.util +import os, io +from ..ImageHelpers.PILHelper import * +import random + + +class StreamDockM18(StreamDock): + """StreamDockM18 device class - supports 15 keys""" + + KEY_COUNT = 15 + KEY_MAP = False + + # Image key mapping: logical key -> hardware key (for setting images) + _IMAGE_KEY_MAP = { + ButtonKey.KEY_1: 11, + ButtonKey.KEY_2: 12, + ButtonKey.KEY_3: 13, + ButtonKey.KEY_4: 14, + ButtonKey.KEY_5: 15, + ButtonKey.KEY_6: 6, + ButtonKey.KEY_7: 7, + ButtonKey.KEY_8: 8, + ButtonKey.KEY_9: 9, + ButtonKey.KEY_10: 10, + ButtonKey.KEY_11: 1, + ButtonKey.KEY_12: 2, + ButtonKey.KEY_13: 3, + ButtonKey.KEY_14: 4, + ButtonKey.KEY_15: 5, + ButtonKey.KEY_16: 0x25, + ButtonKey.KEY_17: 0x30, + ButtonKey.KEY_18: 0x31, + } + + # Reverse mapping: hardware key -> logical key (for event decoding) + _HW_TO_LOGICAL_KEY = {v: k for k, v in _IMAGE_KEY_MAP.items()} + + def __init__(self, transport1, devInfo): + super().__init__(transport1, devInfo) + + 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 + """ + if logical_key in self._IMAGE_KEY_MAP: + return self._IMAGE_KEY_MAP[logical_key] + raise ValueError(f"StreamDockM18: Unsupported key {logical_key}") + + def decode_input_event(self, hardware_code: int, state: int) -> InputEvent: + """ + Decode hardware event codes into a unified InputEvent + + M18 supports only regular buttons; hardware code range 1-15 + """ + # Handle state value: 0x02=release, 0x01=press + normalized_state = 1 if state == 0x01 else 0 + + # Regular button events (1-15) + if hardware_code in self._HW_TO_LOGICAL_KEY: + return InputEvent( + event_type=EventType.BUTTON, + key=self._HW_TO_LOGICAL_KEY[hardware_code], + state=normalized_state + ) + + # Unknown event + return InputEvent(event_type=EventType.UNKNOWN) + + # Set device screen brightness + def set_brightness(self, percent): + return self.transport.setBrightness(percent) + + # Set device background image 480 * 272 + def set_touchscreen_image(self, path): + try: + if not os.path.exists(path): + print(f"Error: The image file '{path}' does not exist.") + return -1 + + # open formatter + image = Image.open(path) + image = to_native_touchscreen_format(self, image) + temp_image_path = ( + "rotated_touchscreen_image_" + + str(random.randint(9999, 999999)) + + ".jpg" + ) + image.save(temp_image_path) + + # encode send + path_bytes = temp_image_path.encode("utf-8") + c_path = ctypes.c_char_p(path_bytes) + res = self.transport.setBackgroundImgDualDevice(c_path) + os.remove(temp_image_path) + return res + + except Exception as e: + print(f"Error: {e}") + return -1 + + # Set device key icon image 64 * 64 + def set_key_image(self, key, path): + try: + if isinstance(key, int): + if key not in range(1, 16): + print(f"key '{key}' out of range. you should set (1 ~ 15)") + return -1 + logical_key = ButtonKey(key) + else: + logical_key = key + + if not os.path.exists(path): + print(f"Error: The image file '{path}' does not exist.") + return -1 + + # Get hardware key value + hardware_key = self.get_image_key(logical_key) + + # open formatter + image = Image.open(path) + image = to_native_key_format(self, image) + temp_image_path = ( + "rotated_key_image_" + str(random.randint(9999, 999999)) + ".jpg" + ) + image.save(temp_image_path) + + # encode send + path_bytes = temp_image_path.encode("utf-8") + c_path = ctypes.c_char_p(path_bytes) + res = self.transport.setKeyImgDualDevice(c_path, hardware_key) + os.remove(temp_image_path) + return res + + except Exception as e: + print(f"Error: {e}") + return -1 + + # TODO + def set_key_imageData(self, key, path): + pass + + # Get device firmware version + def get_serial_number(self): + return self.serial_number + + def key_image_format(self): + return { + "size": (64, 64), + "format": "JPEG", + "rotation": 0, + "flip": (False, False), + } + + def touchscreen_image_format(self): + return { + "size": (480, 272), + "format": "JPEG", + "rotation": 0, + "flip": (False, False), + } + + # Set device parameters + def set_device(self): + self.transport.set_report_size(513, 1025, 0) + self.feature_option.hasRGBLed = True + self.feature_option.ledCounts = 24 + self.feature_option.deviceType = device_type.dock_m18 + pass diff --git a/StreamDock/Devices/StreamDockM3.py b/StreamDock/Devices/StreamDockM3.py new file mode 100644 index 0000000..6d40c82 --- /dev/null +++ b/StreamDock/Devices/StreamDockM3.py @@ -0,0 +1,230 @@ +from StreamDock.FeatrueOption import device_type +from .StreamDock import StreamDock +from ..InputTypes import Direction, InputEvent, ButtonKey, EventType, KnobId +from PIL import Image +import ctypes +import ctypes.util +import os, io +from ..ImageHelpers.PILHelper import * +import random + + +class StreamDockM3(StreamDock): + """StreamDockM3 device class - supports 15 keys""" + + KEY_COUNT = 15 + KEY_MAP = False + + # Image key mapping: logical key -> hardware key (for setting images) + _IMAGE_KEY_MAP = { + ButtonKey.KEY_1: 11, + ButtonKey.KEY_2: 12, + ButtonKey.KEY_3: 13, + ButtonKey.KEY_4: 14, + ButtonKey.KEY_5: 15, + ButtonKey.KEY_6: 6, + ButtonKey.KEY_7: 7, + ButtonKey.KEY_8: 8, + ButtonKey.KEY_9: 9, + ButtonKey.KEY_10: 10, + ButtonKey.KEY_11: 1, + ButtonKey.KEY_12: 2, + ButtonKey.KEY_13: 3, + ButtonKey.KEY_14: 4, + ButtonKey.KEY_15: 5, + } + + # Reverse mapping: hardware key -> logical key (for event decoding) + _HW_TO_LOGICAL_KEY = {v: k for k, v in _IMAGE_KEY_MAP.items()} + + def __init__(self, transport1, devInfo): + super().__init__(transport1, devInfo) + + 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 + """ + if logical_key in self._IMAGE_KEY_MAP: + return self._IMAGE_KEY_MAP[logical_key] + raise ValueError(f"StreamDockM3: Unsupported key {logical_key}") + + def decode_input_event(self, hardware_code: int, state: int) -> InputEvent: + """ + Decode hardware event codes into a unified InputEvent + + M3 supports only regular buttons; hardware code range 1-15 + """ + # Handle state value: 0x02=release, 0x01=press + normalized_state = 1 if state == 0x01 else 0 + + # Regular button events (1-15) + if hardware_code in self._HW_TO_LOGICAL_KEY: + return InputEvent( + event_type=EventType.BUTTON, + key=self._HW_TO_LOGICAL_KEY[hardware_code], + state=normalized_state + ) + # Knob rotation event + knob_rotate_map = { + 0x50: (KnobId.KNOB_1, Direction.LEFT), + 0x51: (KnobId.KNOB_1, Direction.RIGHT), + 0x90: (KnobId.KNOB_2, Direction.LEFT), + 0x91: (KnobId.KNOB_2, Direction.RIGHT), + 0xa0: (KnobId.KNOB_3, Direction.LEFT), + 0xa1: (KnobId.KNOB_3, Direction.RIGHT), + } + if hardware_code in knob_rotate_map: + knob_id, direction = knob_rotate_map[hardware_code] + return InputEvent( + event_type=EventType.KNOB_ROTATE, knob_id=knob_id, direction=direction + ) + + # Knob press event + knob_press_map = { + 0x35: KnobId.KNOB_1, + 0x33: KnobId.KNOB_2, + 0x37: KnobId.KNOB_3, + } + if hardware_code in knob_press_map: + return InputEvent( + event_type=EventType.KNOB_PRESS, + knob_id=knob_press_map[hardware_code], + state=normalized_state, + ) + # Unknown event + return InputEvent(event_type=EventType.UNKNOWN) + + # Set device screen brightness + def set_brightness(self, percent): + return self.transport.setBrightness(percent) + + # Set device background image 480 * 272 + def set_touchscreen_image(self, path): + try: + if not os.path.exists(path): + print(f"Error: The image file '{path}' does not exist.") + return -1 + + # open formatter + image = Image.open(path) + image = to_native_touchscreen_format(self, image) + temp_image_path = ( + "rotated_touchscreen_image_" + + str(random.randint(9999, 999999)) + + ".jpg" + ) + image.save(temp_image_path) + + # encode send + path_bytes = temp_image_path.encode("utf-8") + c_path = ctypes.c_char_p(path_bytes) + res = self.transport.setBackgroundImgDualDevice(c_path) + os.remove(temp_image_path) + return res + + except Exception as e: + print(f"Error: {e}") + return -1 + + # Set device key icon image 64 * 64 + def set_key_image(self, key, path): + try: + if isinstance(key, int): + if key not in range(1, 16): + print(f"key '{key}' out of range. you should set (1 ~ 15)") + return -1 + logical_key = ButtonKey(key) + else: + logical_key = key + + if not os.path.exists(path): + print(f"Error: The image file '{path}' does not exist.") + return -1 + + # Get hardware key value + hardware_key = self.get_image_key(logical_key) + + # open formatter + image = Image.open(path) + image = to_native_key_format(self, image) + temp_image_path = ( + "rotated_key_image_" + str(random.randint(9999, 999999)) + ".jpg" + ) + image.save(temp_image_path) + + # encode send + path_bytes = temp_image_path.encode("utf-8") + c_path = ctypes.c_char_p(path_bytes) + res = self.transport.setKeyImgDualDevice(c_path, hardware_key) + os.remove(temp_image_path) + return res + + except Exception as e: + print(f"Error: {e}") + return -1 + def set_frame_background(self, path): + try: + if not os.path.exists(path): + print(f"Error: The image file '{path}' does not exist.") + return -1 + + image = Image.open(path) + image = to_native_touchscreen_format(self, image) + temp_image_path = ( + "rotated_touchscreen_image_" + + str(random.randint(9999, 999999)) + + ".jpg" + ) + image.save(temp_image_path, quality=80) + + # encode send + path_bytes = temp_image_path.encode("utf-8") + c_path = ctypes.c_char_p(path_bytes) + res = self.transport.setBackgroundImgFrame( + c_path, + 854, + 480, + ) + os.remove(temp_image_path) + return res + except Exception as e: + print(f"Error: {e}") + return -1 + def set_key_imageData(self, key, path): + pass + + def magnetic_calibration(self): + """Perform magnetic calibration.""" + self.transport.magnetic_calibration() + + # Get device firmware version + def get_serial_number(self): + return self.serial_number + + def key_image_format(self): + return { + "size": (96, 96), + "format": "JPEG", + "rotation": 90, + "flip": (False, False), + } + + def touchscreen_image_format(self): + return { + "size": (854, 480), + "format": "JPEG", + "rotation": 90, + "flip": (False, False), + } + + # Set device parameters + def set_device(self): + self.transport.set_report_size(513, 1025, 0) + self.feature_option.deviceType = device_type.dock_m3 + pass diff --git a/StreamDock/Devices/StreamDockN1.py b/StreamDock/Devices/StreamDockN1.py new file mode 100644 index 0000000..d7b6cd2 --- /dev/null +++ b/StreamDock/Devices/StreamDockN1.py @@ -0,0 +1,323 @@ +# -*- coding: utf-8 -*- +from StreamDock.FeatrueOption import device_type +from .StreamDock import StreamDock +from ..InputTypes import InputEvent, ButtonKey, EventType, KnobId, Direction +from PIL import Image +import ctypes +import ctypes.util +import os, io +from ..ImageHelpers.PILHelper import * +import random +from enum import Enum + + +def extract_last_number(code): + """ + Extract consecutive digits after the last dot in a string and convert to int + + Args: + code: String like "N3.02.013" or "N3.02.013V2" + + Returns: + int: Extracted number, or None if not found + """ + # Find the position of the last dot + last_dot = code.rfind(".") + if last_dot == -1: + return None + + # Extract consecutive digits after the last dot + num_str = "" + for char in code[last_dot + 1 :]: + if char.isdigit(): + num_str += char + else: + # Stop at the first non-digit character + break + + # If digits were found, convert to int + if num_str: + return int(num_str) + else: + return None + + +class StreamDockN1(StreamDock): + """StreamDockN1 device class - supports 20 inputs (15 main screen + 3 secondary screen + knob)""" + + KEY_COUNT = 20 + KEY_MAP = False + + # N1 device keys map directly; no mapping needed + _IMAGE_KEY_MAP = { + ButtonKey.KEY_1: 1, + ButtonKey.KEY_2: 2, + ButtonKey.KEY_3: 3, + ButtonKey.KEY_4: 4, + ButtonKey.KEY_5: 5, + ButtonKey.KEY_6: 6, + ButtonKey.KEY_7: 7, + ButtonKey.KEY_8: 8, + ButtonKey.KEY_9: 9, + ButtonKey.KEY_10: 10, + ButtonKey.KEY_11: 11, + ButtonKey.KEY_12: 12, + ButtonKey.KEY_13: 13, + ButtonKey.KEY_14: 14, + ButtonKey.KEY_15: 15, + ButtonKey.KEY_16: 0x1E, + ButtonKey.KEY_17: 0x1F, + } + + class DeviceMode(Enum): + KEYBOARD = 0 + CALCULATOR = 1 + DOCK = 2 + + class SkinMode(Enum): + KEYBOARD = 0x11 + KEYBOARD_LOCK = 0x1F + CALCULATOR = 0xFF + + class SkinStatus(Enum): + PRESS = 0 + RELEASE = 1 + + # Reverse mapping: hardware key -> logical key (for event decoding) + _HW_TO_LOGICAL_KEY = {v: k for k, v in _IMAGE_KEY_MAP.items()} + + def __init__(self, transport1, devInfo): + super().__init__(transport1, devInfo) + self.devInfo = devInfo + + def open(self): + super().open() + self.transport.switchMode(2) + + def get_image_key(self, logical_key: ButtonKey) -> int: + """ + Convert logical key value to hardware key value (for setting images) + + N1 device keys map directly + + Args: + logical_key: Logical key enum + + Returns: + int: Hardware key value + """ + if logical_key in self._IMAGE_KEY_MAP: + return self._IMAGE_KEY_MAP[logical_key] + raise ValueError(f"StreamDockN1: Unsupported key {logical_key}") + + def decode_input_event(self, hardware_code: int, state: int) -> InputEvent: + """ + Decode hardware event codes into a unified InputEvent + + The N1 device supports regular button and knob events: + - Regular buttons 1-17: hardware codes 0x01-0x0F, 0x1E-0x1F + - Knob press 18: hardware code 0x23 + - Knob rotation 19-20: hardware codes 0x32 (left), 0x33 (right) + """ + + # Knob rotation event + knob_rotate_map = { + 0x32: (KnobId.KNOB_1, Direction.LEFT), + 0x33: (KnobId.KNOB_1, Direction.RIGHT), + } + if hardware_code in knob_rotate_map: + knob_id, direction = knob_rotate_map[hardware_code] + return InputEvent( + event_type=EventType.KNOB_ROTATE, knob_id=knob_id, direction=direction + ) + + # Handle state value: 0x02=release, 0x01=press + normalized_state = 1 if state == 0x01 else 0 + # Knob press event + knob_press_map = { + 0x23: KnobId.KNOB_1, + } + if hardware_code in knob_press_map: + return InputEvent( + event_type=EventType.KNOB_PRESS, + knob_id=knob_press_map[hardware_code], + state=normalized_state, + ) + + # Regular button events (1-17) + if hardware_code in self._HW_TO_LOGICAL_KEY: + return InputEvent( + event_type=EventType.BUTTON, + key=self._HW_TO_LOGICAL_KEY[hardware_code], + state=normalized_state, + ) + + # Unknown event + return InputEvent(event_type=EventType.UNKNOWN) + + # Set device screen brightness + def set_brightness(self, percent): + return self.transport.setBrightness(percent) + + # Set device background image 480 * 854 + def set_touchscreen_image(self, path): + version_str = self.get_serial_number() + version_num = extract_last_number(version_str) + if version_num is None: + return -1 + if version_num >= 13: + try: + if not os.path.exists(path): + print(f"Error: The image file '{path}' does not exist.") + return -1 + + # open formatter + image = Image.open(path) + image = to_native_touchscreen_format(self, image) + temp_image_path = ( + "rotated_touchscreen_image_" + + str(random.randint(9999, 999999)) + + ".jpg" + ) + image.save(temp_image_path) + + # encode send + path_bytes = temp_image_path.encode("utf-8") + c_path = ctypes.c_char_p(path_bytes) + res = self.transport.setBackgroundImgDualDevice(c_path) + os.remove(temp_image_path) + return res + except Exception as e: + print(f"Error: {e}") + return -1 + + # Set device key icon image 96 * 96 + def set_key_image(self, key, path): + try: + if isinstance(key, int): + if key not in range(1, 19): + print(f"key '{key}' out of range. you should set (1 ~ 18)") + return -1 + logical_key = ButtonKey(key) + else: + logical_key = key + + if not os.path.exists(path): + print(f"Error: The image file '{path}' does not exist.") + return -1 + + # N1 device keys map directly + hardware_key = logical_key.value + + image = Image.open(path) + if hardware_key in range(1, 16): + # icon + rotated_image = to_native_key_format(self, image) + elif hardware_key in range(16, 19): + # second screen + rotated_image = to_native_seondscreen_format(self, image) + else: + print(f"Error: Invalid hardware key '{hardware_key}'.") + return -1 + rotated_image.save("Temporary.jpg", "JPEG", subsampling=0, quality=90) + returnvalue = self.transport.setKeyImgDualDevice( + bytes("Temporary.jpg", "utf-8"), hardware_key + ) + os.remove("Temporary.jpg") + return returnvalue + + except Exception as e: + print(f"Error: {e}") + return -1 + + def get_serial_number(self): + return self.serial_number + + def switch_mode(self, mode: DeviceMode): + # 0:calculator, 1:dock + return self.transport.switchMode(mode.value) + + def change_page(self, page): + return self.transport.changePage(page) + + def set_n1_skin_bitmap( + self, + path, + skin_mode: SkinMode, + skin_page: int, + skin_status: SkinStatus, + key_index: int, + ): + try: + if not os.path.exists(path): + print(f"Error: The image file '{path}' does not exist.") + return -1 + if self.SkinMode.CALCULATOR == skin_mode: + if key_index < 1 or key_index > 18: + print( + f"Error: For CALCULATOR skin mode, key_index should be in range 1-18." + ) + return -1 + elif ( + self.SkinMode.KEYBOARD == skin_mode + or self.SkinMode.KEYBOARD_LOCK == skin_mode + ): + if key_index < 1 or key_index > 15: + print( + f"Error: For KEYBOARD skin mode, key_index should be in range 1-15." + ) + return -1 + if skin_page < 1 or skin_page > 5: + print(f"Error: skin_page should be in range 1-5.") + return -1 + image = Image.open(path) + if self.SkinMode.KEYBOARD == skin_mode and key_index in range(16, 19): + image = to_native_seondscreen_format(self, image) + else: + image = to_native_key_format(self, image) + temp_image_path = ( + "rotated_n1_skin_image_" + str(random.randint(9999, 999999)) + ".png" + ) + image.save(temp_image_path) + + # encode send + path_bytes = temp_image_path.encode("utf-8") + c_path = ctypes.c_char_p(path_bytes) + res = self.transport.setN1SkinBitMap( + c_path, skin_mode.value, skin_page, skin_status.value, key_index + ) + os.remove(temp_image_path) + return res + except Exception as e: + print(f"Error: {e}") + return -1 + + def key_image_format(self): + return { + "size": (96, 96), + "format": "JPEG", + "rotation": 0, + "flip": (False, False), + } + + def secondscreen_image_format(self): + return { + "size": (80, 80), + "format": "JPEG", + "rotation": 0, + "flip": (False, False), + } + + def touchscreen_image_format(self): + return { + "size": (480, 854), + "format": "JPEG", + "rotation": 0, + "flip": (False, False), + } + + # Set device parameters + def set_device(self): + self.transport.set_report_size(513, 1025, 0) + self.feature_option.deviceType = device_type.dock_n1 + pass diff --git a/StreamDock/Devices/StreamDockN3.py b/StreamDock/Devices/StreamDockN3.py new file mode 100644 index 0000000..f9ba72d --- /dev/null +++ b/StreamDock/Devices/StreamDockN3.py @@ -0,0 +1,142 @@ +from StreamDock.FeatrueOption import device_type +from .StreamDock import StreamDock +from ..InputTypes import InputEvent, ButtonKey, EventType, KnobId, Direction +from PIL import Image +import os +import io +from ..ImageHelpers.PILHelper import * + + +class StreamDockN3(StreamDock): + """StreamDockN3 device class (N3V25 variant) - 6 LCD keys + 3 bottom buttons + 3 knobs""" + + KEY_COUNT = 18 + KEY_MAP = False + + _IMAGE_KEY_MAP = { + ButtonKey.KEY_1: 1, + ButtonKey.KEY_2: 2, + ButtonKey.KEY_3: 3, + ButtonKey.KEY_4: 4, + ButtonKey.KEY_5: 5, + ButtonKey.KEY_6: 6, + ButtonKey.KEY_7: 0x25, + ButtonKey.KEY_8: 0x30, + ButtonKey.KEY_9: 0x31, + } + + _HW_TO_LOGICAL_KEY = {v: k for k, v in _IMAGE_KEY_MAP.items()} + + def __init__(self, transport1, devInfo): + super().__init__(transport1, devInfo) + + def get_image_key(self, logical_key: ButtonKey) -> int: + if logical_key in self._IMAGE_KEY_MAP: + return self._IMAGE_KEY_MAP[logical_key] + raise ValueError(f"StreamDockN3: Unsupported key {logical_key}") + + def decode_input_event(self, hardware_code: int, state: int) -> InputEvent: + normalized_state = 1 if state == 0x01 else 0 + if hardware_code in self._HW_TO_LOGICAL_KEY: + return InputEvent( + event_type=EventType.BUTTON, + key=self._HW_TO_LOGICAL_KEY[hardware_code], + state=normalized_state, + ) + knob_rotate_map = { + 0x90: (KnobId.KNOB_1, Direction.LEFT), + 0x91: (KnobId.KNOB_1, Direction.RIGHT), + 0x60: (KnobId.KNOB_2, Direction.LEFT), + 0x61: (KnobId.KNOB_2, Direction.RIGHT), + 0x50: (KnobId.KNOB_3, Direction.LEFT), + 0x51: (KnobId.KNOB_3, Direction.RIGHT), + } + if hardware_code in knob_rotate_map: + knob_id, direction = knob_rotate_map[hardware_code] + return InputEvent( + event_type=EventType.KNOB_ROTATE, knob_id=knob_id, direction=direction + ) + knob_press_map = { + 0x33: KnobId.KNOB_1, + 0x34: KnobId.KNOB_2, + 0x35: KnobId.KNOB_3, + } + if hardware_code in knob_press_map: + return InputEvent( + event_type=EventType.KNOB_PRESS, + knob_id=knob_press_map[hardware_code], + state=normalized_state, + ) + return InputEvent(event_type=EventType.UNKNOWN) + + def set_brightness(self, percent): + return self.transport.set_key_brightness(percent) + + def set_touchscreen_image(self, path): + try: + if not os.path.exists(path): + print(f"Error: The image file '{path}' does not exist.") + return -1 + image = Image.open(path) + image = to_native_touchscreen_format(self, image) + buf = io.BytesIO() + image.save(buf, format="JPEG", quality=85) + jpeg_data = buf.getvalue() + res = self.transport.set_background_image_stream(jpeg_data) + return res + except Exception as e: + print(f"Error: {e}") + return -1 + + def set_key_image(self, key, path): + try: + if isinstance(key, int): + if key not in range(1, 19): + print(f"key '{key}' out of range. you should set (1 ~ 18)") + return -1 + logical_key = ButtonKey(key) + else: + logical_key = key + + if not os.path.exists(path): + print(f"Error: The image file '{path}' does not exist.") + return -1 + + hardware_key = self.get_image_key(logical_key) + + if hardware_key > 6: + return -1 + + image = Image.open(path) + image = to_native_key_format(self, image) + buf = io.BytesIO() + image.save(buf, format="JPEG", quality=90) + jpeg_data = buf.getvalue() + res = self.transport.set_key_image_stream(jpeg_data, hardware_key) + return res + except Exception as e: + print(f"Error: {e}") + return -1 + + def get_serial_number(self): + return self.serial_number + + def key_image_format(self): + return { + "size": (64, 64), + "format": "JPEG", + "rotation": -90, + "flip": (False, False), + } + + def touchscreen_image_format(self): + return { + "size": (320, 240), + "format": "JPEG", + "rotation": -90, + "flip": (False, False), + } + + def set_device(self): + self.transport.set_report_size(513, 1025, 0) + self.feature_option.deviceType = device_type.dock_n3 \ No newline at end of file diff --git a/StreamDock/Devices/StreamDockN4.py b/StreamDock/Devices/StreamDockN4.py new file mode 100644 index 0000000..bda4f45 --- /dev/null +++ b/StreamDock/Devices/StreamDockN4.py @@ -0,0 +1,210 @@ +from StreamDock.FeatrueOption import device_type +from .StreamDock import StreamDock +from ..InputTypes import InputEvent, ButtonKey, EventType +from PIL import Image +import ctypes +import ctypes.util +import os, io +from ..ImageHelpers.PILHelper import * +import random + + +class StreamDockN4(StreamDock): + """StreamDockN4 device class - supports 14 keys (10 main screen + 4 secondary screen)""" + + KEY_COUNT = 14 + KEY_MAP = False + + # Image key mapping: logical key -> hardware key (for setting images) + _IMAGE_KEY_MAP = { + ButtonKey.KEY_1: 11, + ButtonKey.KEY_2: 12, + ButtonKey.KEY_3: 13, + ButtonKey.KEY_4: 14, + ButtonKey.KEY_5: 15, + ButtonKey.KEY_6: 6, + ButtonKey.KEY_7: 7, + ButtonKey.KEY_8: 8, + ButtonKey.KEY_9: 9, + ButtonKey.KEY_10: 10, + ButtonKey.KEY_11: 1, + ButtonKey.KEY_12: 2, + ButtonKey.KEY_13: 3, + ButtonKey.KEY_14: 4, + } + + # Reverse mapping: hardware key -> logical key (for event decoding) + _HW_TO_LOGICAL_KEY = {v: k for k, v in _IMAGE_KEY_MAP.items()} + + def __init__(self, transport1, devInfo): + super().__init__(transport1, devInfo) + + 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 + """ + if logical_key in self._IMAGE_KEY_MAP: + return self._IMAGE_KEY_MAP[logical_key] + raise ValueError(f"StreamDockN4: Unsupported key {logical_key}") + + def decode_input_event(self, hardware_code: int, state: int) -> InputEvent: + """ + Decode hardware event codes into a unified InputEvent + + The N4 device supports only regular buttons; hardware code range 1-15 + """ + # Handle state value: 0x02=release, 0x01=press + normalized_state = 1 if state == 0x01 else 0 + + # Regular button events (1-14) + if hardware_code in self._HW_TO_LOGICAL_KEY: + return InputEvent( + event_type=EventType.BUTTON, + key=self._HW_TO_LOGICAL_KEY[hardware_code], + state=normalized_state + ) + + # Unknown event + return InputEvent(event_type=EventType.UNKNOWN) + + # Set device screen brightness + def set_brightness(self, percent): + return self.transport.setBrightness(percent) + + # Set device background image 800 * 480 + def set_touchscreen_image(self, path): + try: + if not os.path.exists(path): + print(f"Error: The image file '{path}' does not exist.") + return -1 + + # open formatter + image = Image.open(path) + image = to_native_touchscreen_format(self, image) + temp_image_path = "rotated_touchscreen_image_" + str(random.randint(9999, 999999)) + ".jpg" + image.save(temp_image_path) + + # encode send + path_bytes = temp_image_path.encode('utf-8') + c_path = ctypes.c_char_p(path_bytes) + res = self.transport.setBackgroundImgDualDevice(c_path) + os.remove(temp_image_path) + return res + + except Exception as e: + print(f"Error: {e}") + return -1 + + # Set device key icon image 112 * 112 + def set_key_image(self, key, path): + try: + if isinstance(key, int): + if key not in range(1, 15): + print(f"key '{key}' out of range. you should set (1 ~ 14)") + return -1 + logical_key = ButtonKey(key) + else: + logical_key = key + + if not os.path.exists(path): + print(f"Error: The image file '{path}' does not exist.") + return -1 + + # Secondary screen keys 11-14 + if logical_key.value in range(11, 15): + return self.set_seondscreen_image(logical_key.value, path) + + # Get hardware key value + hardware_key = self.get_image_key(logical_key) + + # open formatter + image = Image.open(path) + image = to_native_key_format(self, image) + temp_image_path = "rotated_key_image_" + str(random.randint(9999, 999999)) + ".jpg" + image.save(temp_image_path) + + # encode send + path_bytes = temp_image_path.encode('utf-8') + c_path = ctypes.c_char_p(path_bytes) + res = self.transport.setKeyImgDualDevice(c_path, hardware_key) + os.remove(temp_image_path) + return res + + except Exception as e: + print(f"Error: {e}") + return -1 + + # Set device secondary screen key icon image 176 * 112 + def set_seondscreen_image(self, key, path): + try: + if key not in range(11, 15): + print(f"key '{key}' out of range. you should set (11 ~ 14)") + return -1 + + logical_key = ButtonKey(key) + hardware_key = self.get_image_key(logical_key) + + if not os.path.exists(path): + print(f"Error: The image file '{path}' does not exist.") + return -1 + + # open formatter + image = Image.open(path) + image = to_native_seondscreen_format(self, image) + temp_image_path = "rotated_key_image_" + str(random.randint(9999, 999999)) + ".jpg" + image.save(temp_image_path) + + # encode send + path_bytes = temp_image_path.encode('utf-8') + c_path = ctypes.c_char_p(path_bytes) + res = self.transport.setKeyImgDualDevice(c_path, hardware_key) + os.remove(temp_image_path) + return res + + except Exception as e: + print(f"Error: {e}") + return -1 + + # TODO + def set_key_imageData(self, key, path): + pass + + # Get device firmware version + def get_serial_number(self): + return self.serial_number + + def key_image_format(self): + return { + 'size': (112, 112), + 'format': "JPEG", + 'rotation': 180, + 'flip': (False, False) + } + + def secondscreen_image_format(self): + return { + 'size': (176, 112), + 'format': "JPEG", + 'rotation': 180, + 'flip': (False, False) + } + + def touchscreen_image_format(self): + return { + 'size': (800, 480), + 'format': "JPEG", + 'rotation': 180, + 'flip': (False, False) + } + + # Set device parameters + def set_device(self): + self.transport.set_report_size(513, 1025, 0) + self.feature_option.deviceType = device_type.dock_n4 + pass diff --git a/StreamDock/Devices/StreamDockN4Pro.py b/StreamDock/Devices/StreamDockN4Pro.py new file mode 100644 index 0000000..a292ea4 --- /dev/null +++ b/StreamDock/Devices/StreamDockN4Pro.py @@ -0,0 +1,306 @@ +from StreamDock.FeatrueOption import device_type +from .StreamDock import StreamDock +from ..InputTypes import InputEvent, ButtonKey, EventType, KnobId, Direction +from PIL import Image +import ctypes +import ctypes.util +import os, io +from ..ImageHelpers.PILHelper import * +import random + + +class StreamDockN4Pro(StreamDock): + """StreamDockN4Pro device class - supports 15 keys, 4 knobs, and swipe gestures""" + + KEY_COUNT = 15 + KEY_MAP = False + + # Image key mapping: logical key -> hardware key (for setting images) + _IMAGE_KEY_MAP = { + # Main keys 1-10 + ButtonKey.KEY_1: 11, + ButtonKey.KEY_2: 12, + ButtonKey.KEY_3: 13, + ButtonKey.KEY_4: 14, + ButtonKey.KEY_5: 15, + ButtonKey.KEY_6: 6, + ButtonKey.KEY_7: 7, + ButtonKey.KEY_8: 8, + ButtonKey.KEY_9: 9, + ButtonKey.KEY_10: 10, + # Secondary screen keys 11-14 (176x112) + ButtonKey.KEY_11: 1, + ButtonKey.KEY_12: 2, + ButtonKey.KEY_13: 3, + ButtonKey.KEY_14: 4, + ButtonKey.KEY_15: 5, + } + + # Reverse mapping: hardware key -> logical key (for event decoding) + _HW_TO_LOGICAL_KEY = {v: k for k, v in _IMAGE_KEY_MAP.items()} + + def __init__(self, transport1, devInfo): + super().__init__(transport1, devInfo) + + 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 + """ + if logical_key in self._IMAGE_KEY_MAP: + return self._IMAGE_KEY_MAP[logical_key] + raise ValueError(f"StreamDockN4Pro: Unsupported key {logical_key}") + + def decode_input_event(self, hardware_code: int, state: int) -> InputEvent: + """ + Decode hardware event codes into a unified InputEvent + + Hardware code mapping: + - Keys: 1-15 + - Secondary screen keys: 0x40-0x43 + - Knob rotation: 0xA0, 0xA1(Knob1), 0x50, 0x51(Knob2), 0x90, 0x91(Knob3), 0x70, 0x71(Knob4) + - Knob press: 0x37(Knob1), 0x35(Knob2), 0x33(Knob3), 0x36(Knob4) + - Swipe: 0x38 (left), 0x39 (right) + """ + # Handle state value: 0x02=release, 0x01=press + normalized_state = 1 if state == 0x01 else 0 + + # Regular button events (1-15) + if hardware_code in self._HW_TO_LOGICAL_KEY: + return InputEvent( + event_type=EventType.BUTTON, + key=self._HW_TO_LOGICAL_KEY[hardware_code], + state=normalized_state, + ) + + # Secondary screen key events + secondary_key_map = { + 0x40: ButtonKey.KEY_11, + 0x41: ButtonKey.KEY_12, + 0x42: ButtonKey.KEY_13, + 0x43: ButtonKey.KEY_14, + } + if hardware_code in secondary_key_map: + return InputEvent( + event_type=EventType.BUTTON, + key=secondary_key_map[hardware_code], + state=normalized_state, + ) + + # Knob rotation event + knob_rotate_map = { + 0xA0: (KnobId.KNOB_1, Direction.LEFT), + 0xA1: (KnobId.KNOB_1, Direction.RIGHT), + 0x50: (KnobId.KNOB_2, Direction.LEFT), + 0x51: (KnobId.KNOB_2, Direction.RIGHT), + 0x90: (KnobId.KNOB_3, Direction.LEFT), + 0x91: (KnobId.KNOB_3, Direction.RIGHT), + 0x70: (KnobId.KNOB_4, Direction.LEFT), + 0x71: (KnobId.KNOB_4, Direction.RIGHT), + } + if hardware_code in knob_rotate_map: + knob_id, direction = knob_rotate_map[hardware_code] + return InputEvent( + event_type=EventType.KNOB_ROTATE, knob_id=knob_id, direction=direction + ) + + # Knob press event + knob_press_map = { + 0x37: KnobId.KNOB_1, + 0x35: KnobId.KNOB_2, + 0x33: KnobId.KNOB_3, + 0x36: KnobId.KNOB_4, + } + if hardware_code in knob_press_map: + return InputEvent( + event_type=EventType.KNOB_PRESS, + knob_id=knob_press_map[hardware_code], + state=normalized_state, + ) + + # Swipe gesture + if hardware_code == 0x38: + return InputEvent(event_type=EventType.SWIPE, direction=Direction.LEFT) + if hardware_code == 0x39: + return InputEvent(event_type=EventType.SWIPE, direction=Direction.RIGHT) + + # Unknown event + return InputEvent(event_type=EventType.UNKNOWN) + + # Set device screen brightness + def set_brightness(self, percent): + return self.transport.setBrightness(percent) + + # Set device background image 800 * 480 + def set_touchscreen_image(self, path): + try: + if not os.path.exists(path): + print(f"Error: The image file '{path}' does not exist.") + return -1 + + image = Image.open(path) + image = to_native_touchscreen_format(self, image) + temp_image_path = ( + "rotated_touchscreen_image_" + + str(random.randint(9999, 999999)) + + ".jpg" + ) + image.save(temp_image_path) + + # encode send + path_bytes = temp_image_path.encode("utf-8") + c_path = ctypes.c_char_p(path_bytes) + res = self.transport.setBackgroundImgDualDevice(c_path) + os.remove(temp_image_path) + return res + + except Exception as e: + print(f"Error: {e}") + return -1 + def set_frame_background(self, path): + try: + if not os.path.exists(path): + print(f"Error: The image file '{path}' does not exist.") + return -1 + + image = Image.open(path) + image = to_native_touchscreen_format(self, image) + temp_image_path = ( + "rotated_touchscreen_image_" + + str(random.randint(9999, 999999)) + + ".jpg" + ) + image.save(temp_image_path, quality=80) + + # encode send + path_bytes = temp_image_path.encode("utf-8") + c_path = ctypes.c_char_p(path_bytes) + res = self.transport.setBackgroundImgFrame( + c_path, + 800, + 480, + ) + os.remove(temp_image_path) + return res + except Exception as e: + print(f"Error: {e}") + return -1 + + # Set device key icon image 112 * 112 + def set_key_image(self, key, path): + try: + if isinstance(key, int): + if key not in range(1, 16): + print(f"key '{key}' out of range. you should set (1 ~ 15)") + return -1 + logical_key = ButtonKey(key) + else: + logical_key = key + + if not os.path.exists(path): + print(f"Error: The image file '{path}' does not exist.") + return -1 + + # Secondary screen keys use a different image format + if logical_key.value in range(11, 15): + return self.set_seondscreen_image(logical_key.value, path) + + # Get hardware key value + hardware_key = self.get_image_key(logical_key) + + # open formatter + image = Image.open(path) + image = to_native_key_format(self, image) + temp_image_path = ( + "rotated_key_image_" + str(random.randint(9999, 999999)) + ".jpg" + ) + image.save(temp_image_path) + + # encode send + path_bytes = temp_image_path.encode("utf-8") + c_path = ctypes.c_char_p(path_bytes) + res = self.transport.setKeyImgDualDevice(c_path, hardware_key) + os.remove(temp_image_path) + return res + + except Exception as e: + print(f"Error: {e}") + return -1 + + # Set device secondary screen key icon image 176 * 112 + def set_seondscreen_image(self, key, path): + try: + if key not in range(11, 15): + print(f"key '{key}' out of range. you should set (11 ~ 14)") + return -1 + + logical_key = ButtonKey(key) + hardware_key = self.get_image_key(logical_key) + + if not os.path.exists(path): + print(f"Error: The image file '{path}' does not exist.") + return -1 + + # open formatter + image = Image.open(path) + image = to_native_seondscreen_format(self, image) + temp_image_path = ( + "rotated_key_image_" + str(random.randint(9999, 999999)) + ".jpg" + ) + image.save(temp_image_path) + + # encode send + path_bytes = temp_image_path.encode("utf-8") + c_path = ctypes.c_char_p(path_bytes) + res = self.transport.setKeyImgDualDevice(c_path, hardware_key) + os.remove(temp_image_path) + return res + + except Exception as e: + print(f"Error: {e}") + return -1 + + # TODO + def set_key_imageData(self, key, path): + pass + + # Get device serial number + def get_serial_number(self): + return self.serial_number + + def key_image_format(self): + return { + "size": (112, 112), + "format": "JPEG", + "rotation": 180, + "flip": (False, False), + } + + def secondscreen_image_format(self): + return { + "size": (176, 112), + "format": "JPEG", + "rotation": 180, + "flip": (False, False), + } + + def touchscreen_image_format(self): + return { + "size": (800, 480), + "format": "JPEG", + "rotation": 180, + "flip": (False, False), + } + + # Set device parameters + def set_device(self): + self.transport.set_report_size(513, 1025, 0) + self.feature_option.hasRGBLed = True + self.feature_option.ledCounts = 4 + self.feature_option.deviceType = device_type.dock_n4pro + pass diff --git a/StreamDock/Devices/StreamDockXL.py b/StreamDock/Devices/StreamDockXL.py new file mode 100644 index 0000000..4e6fb57 --- /dev/null +++ b/StreamDock/Devices/StreamDockXL.py @@ -0,0 +1,242 @@ +from StreamDock.FeatrueOption import device_type +from .StreamDock import StreamDock +from ..InputTypes import InputEvent, ButtonKey, EventType, KnobId, Direction +from PIL import Image +import ctypes +import ctypes.util +import os, io +from ..ImageHelpers.PILHelper import * +import random + + +class StreamDockXL(StreamDock): + """StreamDockXL device class - supports 36 inputs (32 keys + 2 knobs)""" + + KEY_COUNT = 36 + KEY_MAP = False + + # Image key mapping: logical key -> hardware key (for setting images) + _IMAGE_KEY_MAP = { + ButtonKey.KEY_1: 25, + ButtonKey.KEY_2: 26, + ButtonKey.KEY_3: 27, + ButtonKey.KEY_4: 28, + ButtonKey.KEY_5: 29, + ButtonKey.KEY_6: 30, + ButtonKey.KEY_7: 31, + ButtonKey.KEY_8: 32, + ButtonKey.KEY_9: 17, + ButtonKey.KEY_10: 18, + ButtonKey.KEY_11: 19, + ButtonKey.KEY_12: 20, + ButtonKey.KEY_13: 21, + ButtonKey.KEY_14: 22, + ButtonKey.KEY_15: 23, + ButtonKey.KEY_16: 24, + ButtonKey.KEY_17: 9, + ButtonKey.KEY_18: 10, + ButtonKey.KEY_19: 11, + ButtonKey.KEY_20: 12, + ButtonKey.KEY_21: 13, + ButtonKey.KEY_22: 14, + ButtonKey.KEY_23: 15, + ButtonKey.KEY_24: 16, + ButtonKey.KEY_25: 1, + ButtonKey.KEY_26: 2, + ButtonKey.KEY_27: 3, + ButtonKey.KEY_28: 4, + ButtonKey.KEY_29: 5, + ButtonKey.KEY_30: 6, + ButtonKey.KEY_31: 7, + ButtonKey.KEY_32: 8, + } + + # Reverse mapping: hardware key -> logical key (for event decoding) + _HW_TO_LOGICAL_KEY = {v: k for k, v in _IMAGE_KEY_MAP.items()} + + def __init__(self, transport1, devInfo): + super().__init__(transport1, devInfo) + + 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 + """ + if logical_key in self._IMAGE_KEY_MAP: + return self._IMAGE_KEY_MAP[logical_key] + raise ValueError(f"StreamDockXL: Unsupported key {logical_key}") + + def decode_input_event(self, hardware_code: int, state: int) -> InputEvent: + """ + Decode hardware event codes into a unified InputEvent + + XL supports regular button and knob events: + - Regular buttons 1-32: hardware codes 0x19-0x08 + - Left knob up/down 33-34: hardware codes 0x21 (up), 0x23 (down) + - Right knob up/down 35-36: hardware codes 0x24 (up), 0x26 (down) + """ + + knob_rotate_map = { + 0x23: (KnobId.KNOB_1, Direction.LEFT), + 0x21: (KnobId.KNOB_1, Direction.RIGHT), + 0x24: (KnobId.KNOB_2, Direction.LEFT), + 0x26: (KnobId.KNOB_2, Direction.RIGHT), + } + + # Knob rotation event + if hardware_code in knob_rotate_map: + knob_id, direction = knob_rotate_map[hardware_code] + return InputEvent( + event_type=EventType.KNOB_ROTATE, knob_id=knob_id, direction=direction + ) + # Handle state value: 0x02=release, 0x01=press + normalized_state = 1 if state == 0x01 else 0 + + # Regular button events (1-32) + if hardware_code in self._HW_TO_LOGICAL_KEY: + return InputEvent( + event_type=EventType.BUTTON, + key=self._HW_TO_LOGICAL_KEY[hardware_code], + state=normalized_state, + ) + + # Unknown event + return InputEvent(event_type=EventType.UNKNOWN) + + def set_frame_background(self, path): + try: + if not os.path.exists(path): + print(f"Error: The image file '{path}' does not exist.") + return -1 + + image = Image.open(path) + image = to_native_touchscreen_format(self, image) + temp_image_path = ( + "rotated_touchscreen_image_" + + str(random.randint(9999, 999999)) + + ".jpg" + ) + image.save(temp_image_path, quality=80) + + # encode send + path_bytes = temp_image_path.encode("utf-8") + c_path = ctypes.c_char_p(path_bytes) + res = self.transport.setBackgroundImgFrame( + c_path, + 1024, + 600, + ) + os.remove(temp_image_path) + return res + except Exception as e: + print(f"Error: {e}") + return -1 + # Set device screen brightness + def set_brightness(self, percent): + return self.transport.setBrightness(percent) + + # Set device background image 1024 * 600 + def set_touchscreen_image(self, path): + try: + if not os.path.exists(path): + print(f"Error: The image file '{path}' does not exist.") + return -1 + + # open formatter + image = Image.open(path) + image = to_native_touchscreen_format(self, image) + temp_image_path = ( + "rotated_touchscreen_image_" + + str(random.randint(9999, 999999)) + + ".jpg" + ) + image.save(temp_image_path) + + # encode send + path_bytes = temp_image_path.encode("utf-8") + c_path = ctypes.c_char_p(path_bytes) + res = self.transport.setBackgroundImgDualDevice(c_path) + os.remove(temp_image_path) + return res + + except Exception as e: + print(f"Error: {e}") + return -1 + + # Set device key icon image 80 * 80 + def set_key_image(self, key, path): + try: + if isinstance(key, int): + if key not in range(1, 33): + print(f"key '{key}' out of range. you should set (1 ~ 32)") + return -1 + logical_key = ButtonKey(key) + else: + logical_key = key + + if not os.path.exists(path): + print(f"Error: The image file '{path}' does not exist.") + return -1 + + # Get hardware key value + hardware_key = self.get_image_key(logical_key) + + # XL supports setting icons only for keys 1-32 (knob events do not require icons) + if hardware_key not in range(1, 33): + return -1 + + # open formatter + image = Image.open(path) + image = to_native_key_format(self, image) + temp_image_path = ( + "rotated_key_image_" + str(random.randint(9999, 999999)) + ".jpg" + ) + image.save(temp_image_path) + + # encode send + path_bytes = temp_image_path.encode("utf-8") + c_path = ctypes.c_char_p(path_bytes) + res = self.transport.setKeyImgDualDevice(c_path, hardware_key) + os.remove(temp_image_path) + return res + + except Exception as e: + print(f"Error: {e}") + return -1 + + # TODO + def set_key_imageData(self, key, path): + pass + + # Get device firmware version + def get_serial_number(self): + return self.serial_number + + def key_image_format(self): + return { + "size": (80, 80), + "format": "JPEG", + "rotation": 180, + "flip": (False, False), + } + + def touchscreen_image_format(self): + return { + "size": (1024, 600), + "format": "JPEG", + "rotation": 180, + "flip": (False, False), + } + + # Set device parameters + def set_device(self): + self.transport.set_report_size(513, 1025, 0) + self.feature_option.hasRGBLed = True + self.feature_option.ledCounts = 6 + self.feature_option.deviceType = device_type.dock_xl + pass diff --git a/StreamDock/Devices/__init__.py b/StreamDock/Devices/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/StreamDock/FeatrueOption.py b/StreamDock/FeatrueOption.py new file mode 100644 index 0000000..358ac1e --- /dev/null +++ b/StreamDock/FeatrueOption.py @@ -0,0 +1,24 @@ +from enum import Enum + +class device_type(Enum): + dock_universal = 0 + dock_293 = 1 + dock_293v3=2 + dock_293s=3 + dock_293sv3=4 + dock_m3=5 + dock_m18=6 + dock_n1=7 + dock_n3=8 + dock_n4=9 + dock_n4pro=10 + dock_xl=11 + k1pro=12 + +class FeatrueOption: + def __init__(self): + self.hasRGBLed = False + self.ledCounts = 0 + self.supportConfig = False + self.supportBackgroundImage = True + self.deviceType = device_type.dock_universal diff --git a/StreamDock/ImageHelpers/PILHelper.py b/StreamDock/ImageHelpers/PILHelper.py new file mode 100644 index 0000000..426aa10 --- /dev/null +++ b/StreamDock/ImageHelpers/PILHelper.py @@ -0,0 +1,89 @@ +import io +from PIL import Image + + +def _create_image(image_format, background): + return Image.new("RGB", image_format['size'], background) + + +def _scale_image(image, image_format, margins=[0, 0, 0, 0], background='black'): + if len(margins) != 4: + raise ValueError("Margins should be given as an array of four integers.") + + final_image = _create_image(image_format, background=background) + + thumbnail_max_width = final_image.width - (margins[1] + margins[3]) + thumbnail_max_height = final_image.height - (margins[0] + margins[2]) + + thumbnail = image.convert("RGBA") + thumbnail.thumbnail((thumbnail_max_width, thumbnail_max_height), Image.LANCZOS) + + thumbnail_x = (margins[3] + (thumbnail_max_width - thumbnail.width) // 2) + thumbnail_y = (margins[0] + (thumbnail_max_height - thumbnail.height) // 2) + + final_image.paste(thumbnail, (thumbnail_x, thumbnail_y), thumbnail) + + return final_image + + +def _to_native_format(image, image_format): + if image_format["format"].lower() != "jpeg" and image_format["format"].lower() != "jpg": + raise ValueError(f"no support format: {image_format['format']}. only 'jpeg' or 'jpg' is supported") + + _expand = True + if image.size[1] == image_format["size"][0] and image.size[0] == image_format["size"][1]: + _expand = False + + # must rotate the picture first then resize the picture + if image_format["rotation"] == 90 or image_format["rotation"] == -90: + swapped_tuple = (image_format["size"][1], image_format["size"][0]) + image_format["size"] = swapped_tuple + + if image_format['rotation']: + image = image.rotate(image_format['rotation'], expand = _expand) + + if image.size != image_format['size']: + image = image.resize(image_format["size"]) + + if image_format['flip'][0]: + image = image.transpose(Image.FLIP_LEFT_RIGHT) + + if image_format['flip'][1]: + image = image.transpose(Image.FLIP_TOP_BOTTOM) + + image = image.convert('RGB') + + return image + + +def create_image(dock, background='black'): + return create_key_image(dock, background) + + +def create_key_image(dock, background='black'): + return _create_image(dock.key_image_format(), background) + + +def create_touchscreen_image(dock, background='black'): + return _create_image(dock.touchscreen_image_format(), background) + + +def create_scaled_image(dock, image, margins=[0, 0, 0, 0], background='black'): + return create_scaled_key_image(dock, image, margins, background) + + +def create_scaled_key_image(dock, image, margins=[0, 0, 0, 0], background='black'): + return _scale_image(image, dock.key_image_format(), margins, background) + + +def create_scaled_touchscreen_image(dock, image, margins=[0, 0, 0, 0], background='black'): + return _scale_image(image, dock.touchscreen_image_format(), margins, background) + +def to_native_key_format(dock, image): + return _to_native_format(image, dock.key_image_format()) + +def to_native_seondscreen_format(dock, image): + return _to_native_format(image, dock.secondscreen_image_format()) + +def to_native_touchscreen_format(dock, image): + return _to_native_format(image, dock.touchscreen_image_format()) diff --git a/StreamDock/ImageHelpers/__init__.py b/StreamDock/ImageHelpers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/StreamDock/InputTypes.py b/StreamDock/InputTypes.py new file mode 100644 index 0000000..69c116c --- /dev/null +++ b/StreamDock/InputTypes.py @@ -0,0 +1,109 @@ +""" +StreamDock input event type system + +Provides unified input event definitions, including buttons, knobs, and swipe gestures. +""" + +from enum import Enum, IntEnum +from dataclasses import dataclass +from typing import Optional + + +class EventType(Enum): + """Event type enum""" + BUTTON = "button" # Button press/release + KNOB_ROTATE = "knob_rotate" # Knob rotation + KNOB_PRESS = "knob_press" # Knob press + SWIPE = "swipe" # Swipe gesture + UNKNOWN = "unknown" + + +class ButtonKey(IntEnum): + """ + Logical key values for regular buttons (used for setting images) + + Devices can define their own key ranges, for example: + - Simple devices: KEY_1 ~ KEY_15 + - XL devices: KEY_1 ~ KEY_32 + """ + KEY_1 = 1 + KEY_2 = 2 + KEY_3 = 3 + KEY_4 = 4 + KEY_5 = 5 + KEY_6 = 6 + KEY_7 = 7 + KEY_8 = 8 + KEY_9 = 9 + KEY_10 = 10 + KEY_11 = 11 + KEY_12 = 12 + KEY_13 = 13 + KEY_14 = 14 + KEY_15 = 15 + KEY_16 = 16 + KEY_17 = 17 + KEY_18 = 18 + KEY_19 = 19 + KEY_20 = 20 + KEY_21 = 21 + KEY_22 = 22 + KEY_23 = 23 + KEY_24 = 24 + KEY_25 = 25 + KEY_26 = 26 + KEY_27 = 27 + KEY_28 = 28 + KEY_29 = 29 + KEY_30 = 30 + KEY_31 = 31 + KEY_32 = 32 + + +class KnobId(Enum): + """Knob ID enum""" + KNOB_1 = "knob_1" + KNOB_2 = "knob_2" + KNOB_3 = "knob_3" + KNOB_4 = "knob_4" + + +class Direction(Enum): + """Direction enum (for knob rotation and swipe gestures)""" + LEFT = "left" + RIGHT = "right" + + +@dataclass +class InputEvent: + """ + Unified input event class + + All input events (buttons, knobs, swipes) are passed to callbacks through this class. + + Attributes: + event_type: Event type + key: Button event: which key + knob_id: Knob event: which knob + direction: Direction: knob rotation direction or swipe direction + state: State: 0=release, 1=press + """ + event_type: EventType + key: Optional[ButtonKey] = None # Button event: which key + knob_id: Optional[KnobId] = None # Knob event: which knob + direction: Optional[Direction] = None # Direction: knob rotation direction or swipe direction + state: int = 0 # State: 0=release, 1=press + + def __post_init__(self): + """Data validation""" + if self.event_type == EventType.BUTTON: + if self.key is None: + raise ValueError("BUTTON event requires key") + elif self.event_type in (EventType.KNOB_ROTATE, EventType.KNOB_PRESS): + if self.knob_id is None: + raise ValueError("KNOB event requires knob_id") + if self.event_type == EventType.KNOB_ROTATE and self.direction is None: + raise ValueError("KNOB_ROTATE event requires direction") + elif self.event_type == EventType.SWIPE: + if self.direction is None: + raise ValueError("SWIPE event requires direction") diff --git a/StreamDock/ProductIDs.py b/StreamDock/ProductIDs.py new file mode 100644 index 0000000..cc126c4 --- /dev/null +++ b/StreamDock/ProductIDs.py @@ -0,0 +1,129 @@ +class USBVendorIDs: + """ + USB Vendor IDs for known StreamDock devices. + """ + + USB_VID_293 = 0x5500 + USB_VID_293V3 = 0x6603 + USB_VID_293V3EN = 0x6603 + USB_VID_293s = 0x5548 + USB_VID_293sV3 = 0x6603 + USB_VIDN3 = 0x6603 + USB_VIDN3V2 = 0xEEEF + USB_VIDN3V25 = 0x1500 + USB_VIDN3E = 0x6602 + USB_VIDN4 = 0x6602 + USB_VIDN4EN = 0x6603 + USB_VIDN1EN = 0x6603 + USB_VIDN1 = 0x6603 + USB_VID_N4PRO = 0x5548 + USB_VID_N4PROEN = 0x5548 + USB_VID_XL = 0x5548 + USB_VID_XLEN = 0x5548 + USB_VID_M18 = 0x6603 + USB_VID_M18EN = 0x6603 + # USB_VID_M18V2 = 0x6603 + # USB_VID_M18V2EN = 0x6603 + # USB_VID_M18V25 = 0x6603 + # USB_VID_M18V25EN = 0x6603 + # USB_VID_M18V3 = 0x6603 + # USB_VID_M18V3EN = 0x6603 + USB_VID_M3 = 0x5548 + USB_VID_K1_PRO = 0x6603 + USB_VID_K1_PROEU = 0x6603 + + +class USBProductIDs: + """ + USB Product IDs for known StreamDock devices. + """ + + USB_PID_STREAMDOCK_293 = 0x1001 + USB_PID_STREAMDOCK_293V3 = 0x1005 + USB_PID_STREAMDOCK_293V3EN = 0x1006 + USB_PID_STREAMDOCK_293V25 = 0x1010 + USB_PID_STREAMDOCK_293s = 0x6670 + USB_PID_STREAMDOCK_293sV3 = 0x1014 + USB_PID_STREAMDOCK_N3 = 0x1002 + USB_PID_STREAMDOCK_N3EN = 0x1003 + USB_PID_STREAMDOCK_N3V2 = 0x2929 + USB_PID_STREAMDOCK_N3V25 = 0x3001 + USB_PID_STREAMDOCK_N4 = 0x1001 + USB_PID_STREAMDOCK_N4EN = 0x1007 + USB_PID_STREAMDOCK_N1EN = 0x1000 + USB_PID_STREAMDOCK_N1 = 0x1011 + USB_PID_STREAMDOCK_N4PRO = 0x1008 + USB_PID_STREAMDOCK_N4PROEN = 0x1021 + USB_PID_STREAMDOCK_VSD_N4PRO = 0x1023 + USB_PID_STREAMDOCK_XL = 0x1028 + USB_PID_STREAMDOCK_XLEN = 0x1031 + USB_PID_STREAMDOCK_M18 = 0x1009 + USB_PID_STREAMDOCK_M18EN = 0x1012 + # USB_PID_STREAMDOCK_M18V2 = 0x1009 + # USB_PID_STREAMDOCK_M18V2EN = 0x1012 + # USB_PID_STREAMDOCK_M18V25 = 0x1009 + # USB_PID_STREAMDOCK_M18V25EN = 0x1012 + # USB_PID_STREAMDOCK_M18V3 = 0x1009 + # USB_PID_STREAMDOCK_M18V3EN = 0x1012 + USB_PID_STREAMDOCK_M3 = 0x1020 + USB_PID_K1_PRO = 0x1015 + USB_PID_K1_PROEU = 0x1019 + + +from .Devices.StreamDock293 import StreamDock293 +from .Devices.StreamDock293V3 import StreamDock293V3 +from .Devices.StreamDock293s import StreamDock293s +from .Devices.StreamDock293sV3 import StreamDock293sV3 +from .Devices.StreamDockN3 import StreamDockN3 +from .Devices.StreamDockN4 import StreamDockN4 +from .Devices.StreamDockN1 import StreamDockN1 +from .Devices.StreamDockN4Pro import StreamDockN4Pro +from .Devices.StreamDockXL import StreamDockXL +from .Devices.StreamDockM18 import StreamDockM18 +from .Devices.StreamDockM3 import StreamDockM3 +from .Devices.K1Pro import K1Pro + +g_products = [ + # 293 serial + (USBVendorIDs.USB_VID_293, USBProductIDs.USB_PID_STREAMDOCK_293, StreamDock293), + (USBVendorIDs.USB_VID_293V3,USBProductIDs.USB_PID_STREAMDOCK_293V3,StreamDock293V3), + (USBVendorIDs.USB_VID_293V3EN,USBProductIDs.USB_PID_STREAMDOCK_293V3EN,StreamDock293V3), + (USBVendorIDs.USB_VID_293V3,USBProductIDs.USB_PID_STREAMDOCK_293V25,StreamDock293V3), + (USBVendorIDs.USB_VID_293s, USBProductIDs.USB_PID_STREAMDOCK_293s, StreamDock293s), + (USBVendorIDs.USB_VID_293sV3, USBProductIDs.USB_PID_STREAMDOCK_293sV3, StreamDock293sV3), + # N3 + (USBVendorIDs.USB_VIDN3, USBProductIDs.USB_PID_STREAMDOCK_N3, StreamDockN3), + (USBVendorIDs.USB_VIDN3, USBProductIDs.USB_PID_STREAMDOCK_N3EN, StreamDockN3), + (USBVendorIDs.USB_VIDN3E, USBProductIDs.USB_PID_STREAMDOCK_N3, StreamDockN3), + (USBVendorIDs.USB_VIDN3E, USBProductIDs.USB_PID_STREAMDOCK_N3EN, StreamDockN3), + (USBVendorIDs.USB_VIDN3E, USBProductIDs.USB_PID_STREAMDOCK_N3V2, StreamDockN3), + (USBVendorIDs.USB_VIDN3V25, USBProductIDs.USB_PID_STREAMDOCK_N3V25, StreamDockN3), + # N4 + (USBVendorIDs.USB_VIDN4, USBProductIDs.USB_PID_STREAMDOCK_N4, StreamDockN4), + (USBVendorIDs.USB_VIDN4EN, USBProductIDs.USB_PID_STREAMDOCK_N4EN, StreamDockN4), + # N1 + (USBVendorIDs.USB_VIDN1, USBProductIDs.USB_PID_STREAMDOCK_N1, StreamDockN1), + (USBVendorIDs.USB_VIDN1EN, USBProductIDs.USB_PID_STREAMDOCK_N1EN, StreamDockN1), + # N4PRO + (USBVendorIDs.USB_VID_N4PRO, USBProductIDs.USB_PID_STREAMDOCK_N4PRO, StreamDockN4Pro), + (USBVendorIDs.USB_VID_N4PROEN, USBProductIDs.USB_PID_STREAMDOCK_N4PROEN, StreamDockN4Pro), + (USBVendorIDs.USB_VID_N4PRO, USBProductIDs.USB_PID_STREAMDOCK_VSD_N4PRO, StreamDockN4Pro), + # XL + (USBVendorIDs.USB_VID_XL, USBProductIDs.USB_PID_STREAMDOCK_XL, StreamDockXL), + (USBVendorIDs.USB_VID_XLEN, USBProductIDs.USB_PID_STREAMDOCK_XLEN, StreamDockXL), + # M18/M18V2/M18V25/M18V3 + (USBVendorIDs.USB_VID_M18, USBProductIDs.USB_PID_STREAMDOCK_M18, StreamDockM18), + (USBVendorIDs.USB_VID_M18EN, USBProductIDs.USB_PID_STREAMDOCK_M18EN, StreamDockM18), + # (USBVendorIDs.USB_VID_M18V2, USBProductIDs.USB_PID_STREAMDOCK_M18V2, StreamDockM18), + # (USBVendorIDs.USB_VID_M18V2EN, USBProductIDs.USB_PID_STREAMDOCK_M18V2EN, StreamDockM18), + # (USBVendorIDs.USB_VID_M18V25, USBProductIDs.USB_PID_STREAMDOCK_M18V25, StreamDockM18), + # (USBVendorIDs.USB_VID_M18V25EN, USBProductIDs.USB_PID_STREAMDOCK_M18V25EN, StreamDockM18), + # (USBVendorIDs.USB_VID_M18V3, USBProductIDs.USB_PID_STREAMDOCK_M18V3, StreamDockM18), + # (USBVendorIDs.USB_VID_M18V3EN, USBProductIDs.USB_PID_STREAMDOCK_M18V3EN, StreamDockM18), + # M3 + (USBVendorIDs.USB_VID_M3, USBProductIDs.USB_PID_STREAMDOCK_M3, StreamDockM3), + # K1 Pro + (USBVendorIDs.USB_VID_K1_PRO, USBProductIDs.USB_PID_K1_PRO, K1Pro), + (USBVendorIDs.USB_VID_K1_PROEU, USBProductIDs.USB_PID_K1_PROEU, K1Pro), + +] diff --git a/StreamDock/Transport/LibUSBHIDAPI.py b/StreamDock/Transport/LibUSBHIDAPI.py new file mode 100644 index 0000000..9782936 --- /dev/null +++ b/StreamDock/Transport/LibUSBHIDAPI.py @@ -0,0 +1,1398 @@ +""" +LibUSBHIDAPI - Python wrapper for StreamDock Transport C library + +This module provides a Python interface to the StreamDock Transport library, +encapsulating HID device operations such as: +- Device initialization and management +- I/O operations (read/write) +- Image transfer (key and background images) +- LED control +- Device configuration and control + +The wrapper follows RAII-style resource management patterns and provides +a clean, Pythonic interface to the underlying C API. +""" + +import os +import ctypes +import platform +from ctypes import ( + POINTER, + c_size_t, + c_uint8, + c_void_p, + c_char_p, + c_int, + c_ulong, + c_ubyte, + c_uint16, + c_uint32, + c_int32, + c_wchar_p, + c_char, +) +import re +from typing import Optional, List, Tuple + + +def _get_glibc_version() -> Tuple[int, int]: + """ + Get the system's glibc version. + + Returns: + Tuple[int, int]: (major_version, minor_version) + """ + try: + import ctypes.util + + # Try to get glibc version via libc + libc = ctypes.CDLL(ctypes.util.find_library("c"), use_errno=True) + # gnu_get_libc_version() returns a string like "2.39" + gnu_get_libc_version = libc.gnu_get_libc_version + gnu_get_libc_version.restype = c_char_p + version_str = gnu_get_libc_version().decode("utf-8") + parts = version_str.split(".") + if len(parts) >= 2: + return (int(parts[0]), int(parts[1])) + except Exception: + pass + return (2, 0) # Default to a low version if detection fails + + +def _get_dll_name() -> str: + """ + Determine the appropriate transport library name based on platform and architecture. + + For Linux, searches for libraries with glibc version suffixes (e.g., libtransport_glibc2.39.so) + and selects the best match for the system's glibc version. + + Returns: + str: The library filename to load + + Raises: + RuntimeError: If the platform/architecture combination is not supported + """ + search_library_names = { + "Windows": {"x86_64": "transport.dll"}, + "Darwin": {"x86_64": "libtransport.dylib", "arm64": "libtransport_arm64.dylib"}, + } + + platform_name = platform.system() + machine_type = platform.machine().lower() + + if platform_name == "Windows": + return search_library_names["Windows"]["x86_64"] + elif platform_name == "Darwin": + if "x86_64" in machine_type or "amd64" in machine_type: + return search_library_names["Darwin"]["x86_64"] + elif "arm64" in machine_type: + return search_library_names["Darwin"]["arm64"] + elif platform_name == "Linux": + # For Linux, search for glibc-versioned libraries + dll_dir = os.path.join( + os.path.dirname(os.path.abspath(__file__)), "TransportDLL" + ) + + # Determine architecture prefix + if "aarch64" in machine_type or "arm64" in machine_type: + arch_prefix = "arm64" + fallback_name = "libtransport_arm64.so" + elif "x86_64" in machine_type or "amd64" in machine_type: + arch_prefix = "" + fallback_name = "libtransport.so" + else: + raise RuntimeError(f"Unsupported architecture on Linux: {machine_type}") + + # Pattern for glibc-versioned libraries: + # libtransport[_arm64]_glibcX.XX.so or libtransport_glibcX.XX.so + pattern = re.compile(rf"^libtransport(_{arch_prefix})?_glibc(\d+)\.(\d+)\.so$") + + # Search for matching libraries + candidates = [] + if os.path.exists(dll_dir): + for filename in os.listdir(dll_dir): + match = pattern.match(filename) + if match: + major = int(match.group(2)) + minor = int(match.group(3)) + candidates.append((major, minor, filename)) + + if candidates: + # Get system glibc version + sys_glibc = _get_glibc_version() + + # Find the best match: highest version that doesn't exceed system version + best_match = None + for major, minor, filename in sorted( + candidates, key=lambda x: (x[0], x[1]) + ): + if major < sys_glibc[0] or ( + major == sys_glibc[0] and minor <= sys_glibc[1] + ): + best_match = filename + elif best_match is None: + # If no compatible version found, use the lowest version as fallback + best_match = filename + break + + if best_match: + return best_match + + # Fallback to old naming convention + return fallback_name + + raise RuntimeError( + f"Unsupported platform/architecture: {platform_name} / {machine_type}" + ) + + +# Load the transport library +_dll_name = _get_dll_name() +_dll_path = os.path.join( + os.path.dirname(os.path.abspath(__file__)), "TransportDLL", _dll_name +) +_transport_lib = ctypes.CDLL(_dll_path) + + +class _HidDeviceInfo(ctypes.Structure): + """ + Structure definition for the hid_device_info structure defined + in the HIDAPI library. + """ + + pass + + +_HidDeviceInfo._fields_ = [ + ("path", ctypes.c_char_p), + ("vendor_id", ctypes.c_ushort), + ("product_id", ctypes.c_ushort), + ("serial_number", ctypes.c_wchar_p), + ("release_number", ctypes.c_ushort), + ("manufacturer_string", ctypes.c_wchar_p), + ("product_string", ctypes.c_wchar_p), + ("usage_page", ctypes.c_ushort), + ("usage", ctypes.c_ushort), + ("interface_number", ctypes.c_int), + ("next", ctypes.POINTER(_HidDeviceInfo)), +] + + +# Define C function signatures +_transport_lib.transport_create.restype = c_uint32 # TransportResult +_transport_lib.transport_create.argtypes = [POINTER(_HidDeviceInfo), POINTER(c_void_p)] + +_transport_lib.transport_destroy.restype = c_uint32 # TransportResult +_transport_lib.transport_destroy.argtypes = [c_void_p] + +_transport_lib.transport_get_firmware_version.restype = c_uint32 +_transport_lib.transport_get_firmware_version.argtypes = [c_void_p, c_char_p, c_size_t] + +_transport_lib.transport_clear_task_queue.restype = c_uint32 +_transport_lib.transport_clear_task_queue.argtypes = [c_void_p] + +_transport_lib.transport_can_write.restype = c_uint32 +_transport_lib.transport_can_write.argtypes = [c_void_p, POINTER(c_int)] + +_transport_lib.transport_read.restype = c_uint32 # TransportResult +_transport_lib.transport_read.argtypes = [ + c_void_p, + POINTER(c_uint8), + POINTER(c_size_t), + c_int32, +] + +_transport_lib.transport_wakeup_screen.restype = c_uint32 +_transport_lib.transport_wakeup_screen.argtypes = [c_void_p] + +_transport_lib.transport_magnetic_calibration.restype = c_uint32 +_transport_lib.transport_magnetic_calibration.argtypes = [c_void_p] + +_transport_lib.transport_set_key_brightness.restype = c_uint32 +_transport_lib.transport_set_key_brightness.argtypes = [c_void_p, c_uint8] + +_transport_lib.transport_clear_all_keys.restype = c_uint32 +_transport_lib.transport_clear_all_keys.argtypes = [c_void_p] + +_transport_lib.transport_clear_key.restype = c_uint32 +_transport_lib.transport_clear_key.argtypes = [c_void_p, c_uint8] + +_transport_lib.transport_refresh.restype = c_uint32 +_transport_lib.transport_refresh.argtypes = [c_void_p] + +_transport_lib.transport_sleep.restype = c_uint32 +_transport_lib.transport_sleep.argtypes = [c_void_p] + +_transport_lib.transport_disconnected.restype = c_uint32 +_transport_lib.transport_disconnected.argtypes = [c_void_p] + +_transport_lib.transport_heartbeat.restype = c_uint32 +_transport_lib.transport_heartbeat.argtypes = [c_void_p] + +_transport_lib.transport_set_background_bitmap.restype = c_uint32 +_transport_lib.transport_set_background_bitmap.argtypes = [ + c_void_p, + c_char_p, + c_size_t, + c_uint32, +] + +_transport_lib.transport_set_key_image_stream.restype = c_uint32 +_transport_lib.transport_set_key_image_stream.argtypes = [ + c_void_p, + c_char_p, + c_size_t, + c_uint8, +] + +_transport_lib.transport_set_background_image_stream.restype = c_uint32 +_transport_lib.transport_set_background_image_stream.argtypes = [ + c_void_p, + c_char_p, + c_size_t, + c_uint32, +] + +_transport_lib.transport_set_background_frame_stream.restype = c_uint32 +_transport_lib.transport_set_background_frame_stream.argtypes = [ + c_void_p, + c_char_p, + c_size_t, + c_uint16, + c_uint16, + c_uint16, + c_uint16, + c_uint8, +] + +_transport_lib.transport_clear_background_frame_stream.restype = c_uint32 +_transport_lib.transport_clear_background_frame_stream.argtypes = [c_void_p, c_uint8] + +_transport_lib.transport_set_led_brightness.restype = c_uint32 +_transport_lib.transport_set_led_brightness.argtypes = [c_void_p, c_uint8] + +_transport_lib.transport_set_led_color.restype = c_uint32 +_transport_lib.transport_set_led_color.argtypes = [ + c_void_p, + c_uint16, + c_uint8, + c_uint8, + c_uint8, +] + +_transport_lib.transport_reset_led_color.restype = c_uint32 +_transport_lib.transport_reset_led_color.argtypes = [c_void_p] + +_transport_lib.transport_set_device_config.restype = c_uint32 +_transport_lib.transport_set_device_config.argtypes = [ + c_void_p, + POINTER(c_uint8), + c_size_t, +] + +_transport_lib.transport_change_mode.restype = c_uint32 +_transport_lib.transport_change_mode.argtypes = [c_void_p, c_uint8] + +_transport_lib.transport_change_page.restype = c_uint32 +_transport_lib.transport_change_page.argtypes = [c_void_p, c_uint8] + +_transport_lib.transport_set_n1_skin_bitmap.restype = c_uint32 +_transport_lib.transport_set_n1_skin_bitmap.argtypes = [ + c_void_p, + c_char_p, + c_size_t, + c_uint8, + c_uint8, + c_uint8, + c_uint8, + c_int32, +] + +_transport_lib.transport_set_reportID.restype = c_uint32 +_transport_lib.transport_set_reportID.argtypes = [c_void_p, c_uint8] + +_transport_lib.transport_reportID.restype = c_uint32 +_transport_lib.transport_reportID.argtypes = [c_void_p, POINTER(c_uint8)] + +_transport_lib.transport_set_reportSize.restype = c_uint32 +_transport_lib.transport_set_reportSize.argtypes = [ + c_void_p, + c_uint16, + c_uint16, + c_uint16, +] + +_transport_lib.transport_raw_hid_last_error.restype = c_uint32 +_transport_lib.transport_raw_hid_last_error.argtypes = [ + c_void_p, + ctypes.c_void_p, + POINTER(c_size_t), +] + +_transport_lib.transport_disable_output.restype = c_uint32 +_transport_lib.transport_disable_output.argtypes = [ctypes.c_int8] + +# ========== Keyboard Lighting Functions ========== +_transport_lib.transport_set_keyboard_backlight_brightness.restype = c_uint32 +_transport_lib.transport_set_keyboard_backlight_brightness.argtypes = [ + c_void_p, + c_uint8, +] + +_transport_lib.transport_set_keyboard_lighting_effects.restype = c_uint32 +_transport_lib.transport_set_keyboard_lighting_effects.argtypes = [c_void_p, c_uint8] + +_transport_lib.transport_set_keyboard_lighting_speed.restype = c_uint32 +_transport_lib.transport_set_keyboard_lighting_speed.argtypes = [c_void_p, c_uint8] + +_transport_lib.transport_set_keyboard_rgb_backlight.restype = c_uint32 +_transport_lib.transport_set_keyboard_rgb_backlight.argtypes = [ + c_void_p, + c_uint8, + c_uint8, + c_uint8, +] + +_transport_lib.transport_keyboard_os_mode_switch.restype = c_uint32 +_transport_lib.transport_keyboard_os_mode_switch.argtypes = [c_void_p, c_uint8] + +# Add missing function signature +_transport_lib.transport_get_last_error_info.restype = c_uint32 # TransportResult +_transport_lib.transport_get_last_error_info.argtypes = [ + c_void_p, + c_void_p, +] # TransportErrorInfo* + +# Load hidapi functions directly from the transport library +# This prevents conflicts with Python's hidapi package +try: + _transport_lib.transport_hid_enumerate.restype = POINTER(_HidDeviceInfo) + _transport_lib.transport_hid_enumerate.argtypes = [c_uint16, c_uint16] + + _transport_lib.transport_hid_free_enumeration.restype = None + _transport_lib.transport_hid_free_enumeration.argtypes = [POINTER(_HidDeviceInfo)] + + _HID_API_AVAILABLE = True +except AttributeError: + _HID_API_AVAILABLE = False + print("Warning: hidapi functions not available in transport library") + + +class LibUSBHIDAPI: + """ + Python wrapper for the StreamDock Transport C library. + + This class provides a high-level, Pythonic interface to the underlying C transport library, + managing HID device operations with RAII-style resource management. + + Features: + - Device initialization and cleanup + - I/O operations (read/write) + - Image transfer (key images, background images, bitmap streams) + - LED control (brightness, color) + - Device configuration and mode switching + - Firmware version retrieval + + Example: + device_info = get_device_info() # from hidapi + device = LibUSBHIDAPI(device_info) + device.set_led_color(1, 255, 0, 0) # Set first LED to red + device.set_key_brightness(50) + firmware = device.get_firmware_version() + """ + + def __init__(self, device_info: Optional[_HidDeviceInfo] = None): + """ + Initialize the transport wrapper. + + Args: + device_info: HID device information structure. If None, creates an uninitialized handle. + """ + self._handle = None + self._input_report_size = 0 + self._output_report_size = 0 + self._feature_report_size = 0 + # CRITICAL: Store device_info properly for resource management + self._device_info = device_info + self._is_open = False + + # Don't create handle immediately, wait for open() call + # This maintains compatibility with the existing StreamDock API + + def __del__(self): + """ + Destructor - automatically releases the transport handle. + + CRITICAL: This is called during garbage collection which may happen + during interpreter shutdown. We need to be extremely careful about + calling C code here as it can cause segmentation faults. + """ + # Only destroy if we have a handle and Python interpreter is still running + if self._handle: + try: + # Check if Python interpreter is shutting down + import sys + + if sys.is_finalizing(): + # During interpreter shutdown, skip C calls to avoid segfault + # The OS will clean up resources when process exits + return + _transport_lib.transport_destroy(self._handle) + except (AttributeError, TypeError, ValueError): + # C library may already be unloaded or corrupted state + # Silently skip to avoid cascading failures during shutdown + pass + finally: + self._handle = None + + def __enter__(self): + """Context manager support.""" + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + """Context manager cleanup.""" + self.__del__() + return False + + # ========== Device Information and Status ========== + + def get_firmware_version(self) -> str: + """ + Get the firmware version string from the device. + + Returns: + str: Firmware version string + """ + if not self._handle: + return "" + + buffer_size = 64 + buffer = ctypes.create_string_buffer(buffer_size) + result = _transport_lib.transport_get_firmware_version( + self._handle, buffer, buffer_size + ) + if result != 0: + return "" + raw = buffer.raw + + parts = raw.split(b"\x00") + decoded = "" + for part in parts: + if part: + try: + decoded = part.decode("utf-8", errors="ignore") + break + except Exception: + # fallback to continue searching + continue + + return decoded + + def clear_task_queue(self) -> None: + """Clear all pending data in the transport library's task queue.""" + if not self._handle: + return + _transport_lib.transport_clear_task_queue(self._handle) + + def can_write(self) -> bool: + """ + Check if the device is currently writable. + + Returns: + bool: True if device can accept write operations + """ + if not self._handle: + return False + can_write_val = c_int() + result = _transport_lib.transport_can_write( + self._handle, ctypes.byref(can_write_val) + ) + if result != 0: + return False + return bool(can_write_val.value) + + def read(self, timeout_ms: int = -1) -> Optional[bytes]: + """ + Read data from the device. + + Args: + timeout_ms: Timeout in milliseconds. -1 means blocking read. + + Returns: + bytes: Data read from device, or None if error occurred + """ + if not self._handle: + return None + + buffer_size = max(self._input_report_size, 1024) + response = (c_uint8 * buffer_size)() + length = c_size_t(buffer_size) + + result = _transport_lib.transport_read( + self._handle, response, ctypes.byref(length), timeout_ms + ) + + if result == 0: # TRANSPORT_SUCCESS is 0 + return bytes(response[: length.value]) + return None + + # ========== Screen Control ========== + + def wakeup_screen(self) -> None: + """Wake up the device screen.""" + if not self._handle: + return + _transport_lib.transport_wakeup_screen(self._handle) + + def magnetic_calibration(self) -> None: + """Perform magnetic calibration.""" + if not self._handle: + return + _transport_lib.transport_magnetic_calibration(self._handle) + + def refresh_screen(self) -> None: + """Refresh the screen display.""" + if not self._handle: + return + _transport_lib.transport_refresh(self._handle) + + def sleep(self) -> None: + """Put the device into sleep mode.""" + if not self._handle: + return + _transport_lib.transport_sleep(self._handle) + + # ========== Key Control ========== + + def set_key_brightness(self, brightness: int) -> None: + """ + Set the brightness of keys. + + Args: + brightness: Brightness value, typically 0-100 + """ + if not self._handle: + return + _transport_lib.transport_set_key_brightness(self._handle, brightness) + + def clear_all_keys(self) -> None: + """Clear all keys on the device.""" + if not self._handle: + return + _transport_lib.transport_clear_all_keys(self._handle) + + def clear_key(self, key_index: int) -> None: + """ + Clear the content of a specific key. + + Args: + key_index: Index of the key to clear + """ + if not self._handle: + return + _transport_lib.transport_clear_key(self._handle, key_index) + + # ========== Image Transfer ========== + + def set_background_bitmap(self, bitmap_data: bytes, timeout_ms: int = 5000) -> None: + """ + Set the full-screen background using raw bitmap data. + + Args: + bitmap_data: Raw bitmap bytes + timeout_ms: Transmission timeout in milliseconds + """ + if not self._handle: + return + _transport_lib.transport_set_background_bitmap( + self._handle, bitmap_data, len(bitmap_data), timeout_ms + ) + + def set_key_image_stream(self, jpeg_data: bytes, key_index: int) -> None: + """ + Set a JPEG image to a specific key. + + Args: + jpeg_data: JPEG image data + key_index: Target key index + """ + if not self._handle: + return + res = _transport_lib.transport_set_key_image_stream( + self._handle, jpeg_data, len(jpeg_data), key_index + ) + return res + + def set_background_image_stream( + self, jpeg_data: bytes, timeout_ms: int = 3000 + ) -> None: + """ + Set a JPEG image as full-screen background. + + Args: + jpeg_data: JPEG image data + timeout_ms: Transmission timeout in milliseconds + """ + if not self._handle: + return + _transport_lib.transport_set_background_image_stream( + self._handle, jpeg_data, len(jpeg_data), timeout_ms + ) + + def set_background_frame_stream( + self, + jpeg_data: bytes, + width: int, + height: int, + x: int = 0, + y: int = 0, + fb_layer: int = 0x00, + ) -> None: + """ + Draw a JPEG frame at a specific position (used for animated backgrounds). + + Args: + jpeg_data: JPEG image data + width: Image width + height: Image height + x: X-coordinate position + y: Y-coordinate position + fb_layer: Framebuffer layer index + """ + if not self._handle: + return + _transport_lib.transport_set_background_frame_stream( + self._handle, jpeg_data, len(jpeg_data), width, height, x, y, fb_layer + ) + + def clear_background_frame_stream(self, position: int = 0x03) -> None: + """ + Clear background frame on the specified framebuffer layer. + + Args: + position: Layer index (default 0x03) + """ + if not self._handle: + return + _transport_lib.transport_clear_background_frame_stream(self._handle, position) + + # ========== LED Control ========== + + def set_led_brightness(self, brightness: int) -> None: + """ + Set LED brightness. + + Args: + brightness: Brightness value, typically 0-100 + """ + if not self._handle: + return + _transport_lib.transport_set_led_brightness(self._handle, brightness) + + def set_led_color(self, count: int, r: int, g: int, b: int) -> None: + """ + Set color for the first N LEDs. + + Args: + count: Number of LEDs to set + r: Red component (0-255) + g: Green component (0-255) + b: Blue component (0-255) + """ + if not self._handle: + return + res = _transport_lib.transport_set_led_color(self._handle, count, r, g, b) + + def reset_led_color(self) -> None | int: + """Reset LED colors to default.""" + if not self._handle: + return + res = _transport_lib.transport_reset_led_color(self._handle) + return res + + # ========== Keyboard Control ========== + + def set_keyboard_backlight_brightness(self, brightness: int) -> None: + """ + Set the keyboard backlight brightness. + + Args: + brightness: Brightness value (0-6) + """ + if not self._handle: + return + _transport_lib.transport_set_keyboard_backlight_brightness( + self._handle, brightness + ) + + def set_keyboard_lighting_effects(self, effect: int) -> None: + """ + Set the keyboard lighting effect. + 0 is static lighting. + Args: + effect: Effect mode identifier (0-9) + """ + if not self._handle: + return + _transport_lib.transport_set_keyboard_lighting_effects(self._handle, effect) + + def set_keyboard_lighting_speed(self, speed: int) -> None: + """ + Set the keyboard lighting effect speed. + + Args: + speed: Speed value for lighting effects (0-7) + """ + if not self._handle: + return + _transport_lib.transport_set_keyboard_lighting_speed(self._handle, speed) + + def set_keyboard_rgb_backlight(self, red: int, green: int, blue: int) -> None: + """ + Set the keyboard RGB backlight color. + + Args: + red: Red component (0-255) + green: Green component (0-255) + blue: Blue component (0-255) + """ + if not self._handle: + return + _transport_lib.transport_set_keyboard_rgb_backlight( + self._handle, red, green, blue + ) + + def keyboard_os_mode_switch(self, os_mode: int) -> None: + """ + Switch the keyboard OS mode. + + Args: + os_mode: OS mode enum value (e.g., 0 for Windows, 1 for macOS) + """ + if not self._handle: + return + _transport_lib.transport_keyboard_os_mode_switch(self._handle, os_mode) + + # ========== Device Configuration ========== + + def set_device_config(self, configs: List[int]) -> None: + """ + Send raw configuration data to the device. + + Args: + configs: List of configuration byte values + """ + if not self._handle: + return + config_array = (c_uint8 * len(configs))(*configs) + _transport_lib.transport_set_device_config( + self._handle, config_array, len(configs) + ) + + def change_mode(self, mode: int) -> None: + """ + Change device working mode. + + Args: + mode: Mode identifier + """ + if not self._handle: + return + _transport_lib.transport_change_mode(self._handle, mode) + + def change_page(self, page: int) -> None: + """ + Change N1 device calculator mode working page. + + Args: + page: Page identifier + """ + if not self._handle: + return + _transport_lib.transport_change_page(self._handle, page) + + def set_n1_skin_bitmap( + self, + jpeg_data: bytes, + skin_mode: int, + skin_page: int, + skin_status: int, + key_index: int, + timeout_ms: int = 3000, + ) -> None: + """ + Set N1 skin bitmap for a specific mode, page, and key. + Args: + jpeg_data: JPEG image data for the skin + skin_mode: Skin mode identifier, 0 for keyboard, 1 for keyboard lock, 2 for calculator + skin_page: Skin page identifier, 1-5 + skin_status: Skin status identifier, 0 for press, 1 for release + key_index: Target key index for the skin, calculator (1-15), keyboard (1-18) + timeout_ms: Transmission timeout in milliseconds + """ + if not self._handle: + return + _transport_lib.transport_set_n1_skin_bitmap( + self._handle, + jpeg_data, + len(jpeg_data), + skin_mode, + skin_page, + skin_status, + key_index, + timeout_ms, + ) + + def notify_disconnected(self) -> None: + """Notify the device of disconnection.""" + if not self._handle: + return + _transport_lib.transport_disconnected(self._handle) + + def heartbeat(self) -> None: + """Send a heartbeat packet to the device.""" + if not self._handle: + return + _transport_lib.transport_heartbeat(self._handle) + + # ========== Report Configuration ========== + + def set_report_id(self, report_id: int) -> None: + """ + Set the report ID used for communication. + + Args: + report_id: Report ID value (default is typically 0x01) + """ + if not self._handle: + return + _transport_lib.transport_set_reportID(self._handle, report_id) + + def get_report_id(self) -> int: + """ + Get the current report ID. + + Returns: + int: Current report ID value + """ + if not self._handle: + return 0x00 + out_id = c_uint8() + result = _transport_lib.transport_reportID(self._handle, ctypes.byref(out_id)) + if result != 0: + return 0x00 + return int(out_id.value) + + def set_report_size( + self, input_report_size: int, output_report_size: int, feature_report_size: int + ) -> None: + """ + Set the sizes of the input, output, and feature reports. + + Args: + input_report_size: Input report length + output_report_size: Output report length + feature_report_size: Feature report length + """ + if not self._handle: + return + self._input_report_size = input_report_size + self._output_report_size = output_report_size + self._feature_report_size = feature_report_size + _transport_lib.transport_set_reportSize( + self._handle, input_report_size, output_report_size, feature_report_size + ) + + # ========== Error Handling ========== + + def get_last_error(self) -> str: + """ + Get the last raw HID error message. + + Returns: + str: Error message string + """ + if not self._handle: + return "" + + buffer_size = 256 + buffer = ctypes.create_unicode_buffer(buffer_size) + length = c_size_t(buffer_size) + result = _transport_lib.transport_raw_hid_last_error( + self._handle, ctypes.cast(buffer, ctypes.c_void_p), ctypes.byref(length) + ) + if result != 0: + return "" + return buffer.value + + def get_last_error_info(self) -> dict: + """ + Get detailed error information from the transport library. + + Returns: + dict: Error information containing error_code, error_message, function_name, timestamp, and line_number + """ + if not self._handle: + return {} + + # Define TransportErrorInfo structure + class TransportErrorInfo(ctypes.Structure): + _fields_ = [ + ("error_code", c_uint32), + ("error_message", c_char * 256), + ("function_name", c_char * 64), + ("timestamp", c_uint32), + ("line_number", c_uint32), + ] + + error_info = TransportErrorInfo() + result = _transport_lib.transport_get_last_error_info( + self._handle, ctypes.byref(error_info) + ) + + if result == 0: # TRANSPORT_SUCCESS is 0 + return { + "error_code": error_info.error_code, + "error_message": error_info.error_message.decode( + "utf-8", errors="ignore" + ), + "function_name": error_info.function_name.decode( + "utf-8", errors="ignore" + ), + "timestamp": error_info.timestamp, + "line_number": error_info.line_number, + } + return {} + + # ========== Static Methods ========== + + @staticmethod + def disable_output(disable: bool = True) -> None: + """ + Globally disable lower-level output (e.g., debug logs). + + Args: + disable: Whether to disable output + """ + _transport_lib.transport_disable_output(1 if disable else 0) + + @staticmethod + def create_device_info_from_dict(device_dict: dict) -> _HidDeviceInfo: + """ + Create a _HidDeviceInfo structure from a device dictionary. + + Args: + device_dict: Device information dictionary + + Returns: + _HidDeviceInfo structure + """ + device_info = _HidDeviceInfo() + path = device_dict.get("path", "") + device_info.path = path.encode("utf-8") if isinstance(path, str) else path + device_info.vendor_id = device_dict.get("vendor_id", 0) + device_info.product_id = device_dict.get("product_id", 0) + device_info.serial_number = device_dict.get("serial_number", "") + device_info.release_number = device_dict.get("release_number", 0) + device_info.manufacturer_string = device_dict.get("manufacturer_string", "") + device_info.product_string = device_dict.get("product_string", "") + device_info.usage_page = device_dict.get("usage_page", 0) + device_info.usage = device_dict.get("usage", 0) + device_info.interface_number = device_dict.get("interface_number", 0) + device_info.next = None + return device_info + + @staticmethod + def enumerate_devices(vendor_id: int, product_id: int) -> List[dict]: + """ + Enumerate HID devices matching the given vendor and product IDs. + + Use the C library's built-in hidapi for enumeration to avoid conflicts with Python's hidapi package. + + Args: + vendor_id: USB vendor ID + product_id: USB product ID + + Returns: + List of device information dictionaries + """ + device_list = [] + # Use C library's hidapi to avoid conflicts + dev_info_ptr = _transport_lib.transport_hid_enumerate(vendor_id, product_id) + + if not dev_info_ptr: + return device_list + + try: + current = dev_info_ptr + while current: + info = current.contents + if info.usage_page > 1025 and info.usage == 1: + device_list.append( + { + "path": info.path.decode("utf-8") if info.path else "", + "vendor_id": info.vendor_id, + "product_id": info.product_id, + "serial_number": ( + info.serial_number if info.serial_number else "" + ), + "manufacturer_string": ( + info.manufacturer_string + if info.manufacturer_string + else "" + ), + "product_string": ( + info.product_string if info.product_string else "" + ), + "release_number": info.release_number, + "usage_page": info.usage_page, + "usage": info.usage, + "interface_number": info.interface_number, + } + ) + current = info.next + finally: + # Free the enumeration list + _transport_lib.transport_hid_free_enumeration(dev_info_ptr) + + return device_list + + # ========== Properties ========== + + @property + def input_report_size(self) -> int: + """Get the input report size.""" + return self._input_report_size + + @property + def output_report_size(self) -> int: + """Get the output report size.""" + return self._output_report_size + + @property + def feature_report_size(self) -> int: + """Get the feature report size.""" + return self._feature_report_size + + # ========== Legacy Method Aliases (for backward compatibility) ========== + + def getFirmwareVersion(self) -> str: + """Legacy alias for get_firmware_version().""" + return self.get_firmware_version() + + def clearTaskQueue(self) -> None: + """Legacy alias for clear_task_queue().""" + self.clear_task_queue() + + def wakeScreen(self) -> None: + """Legacy alias for wakeup_screen().""" + self.wakeup_screen() + + def keyClear(self, index: int) -> None: + """Legacy alias for clear_key().""" + self.clear_key(index) + + def keyAllClear(self) -> None: + """Legacy alias for clear_all_keys().""" + self.clear_all_keys() + + def changePage(self, page: int) -> None: + """Legacy alias for change_page().""" + self.change_page(page) + + def switchMode(self, mode: int) -> None: + """Legacy alias for change_mode().""" + self.change_mode(mode) + + def setN1SkinBitMap( + self, + path, + skin_mode: int, + skin_page: int, + skin_status: int, + key_index: int, + ) -> None: + """ + Legacy method to set N1 skin bitmap from an image file path. + Args: + path: Path to the image file (can be str, bytes, c_char_p, or os.PathLike) + skin_mode: Skin mode identifier, 0 for keyboard, 1 for keyboard lock, 2 for calculator + skin_page: Skin page identifier, 1-5 + skin_status: Skin status identifier, 0 for press, 1 for release + key_index: Target key index for the skin, calculator (1-15), keyboard (1-18) + """ + try: + # Convert c_char_p to string if needed + if isinstance(path, c_char_p): + path = ( + path.value.decode("utf-8") + if isinstance(path.value, bytes) + else path.value + ) + elif isinstance(path, bytes): + path = path.decode("utf-8") + + if path is None: + raise ValueError("Path cannot be None") + + with open(path, "rb") as f: + jpeg_data = f.read() + self.set_n1_skin_bitmap( + jpeg_data, skin_mode, skin_page, skin_status, key_index + ) + except Exception as e: + raise RuntimeError(f"Failed to load image from {path}: {e}") + + def open(self, device_path: bytes) -> bool: + """ + Open a device connection using the device path. + + Args: + device_path: Device path as bytes + """ + if self._is_open or self._handle is not None: + # Already opened + print("[WARNING] Device already open", flush=True) + return False + + # Create device info structure from path + device_info = _HidDeviceInfo() + device_info.path = device_path + + if self._device_info: + # Use stored device info for other fields + device_info.vendor_id = self._device_info.vendor_id + device_info.product_id = self._device_info.product_id + device_info.serial_number = self._device_info.serial_number + device_info.release_number = self._device_info.release_number + device_info.manufacturer_string = self._device_info.manufacturer_string + device_info.product_string = self._device_info.product_string + device_info.usage_page = self._device_info.usage_page + device_info.usage = self._device_info.usage + device_info.interface_number = self._device_info.interface_number + + # Create the transport handle + handle_ptr = c_void_p() + result = _transport_lib.transport_create( + ctypes.byref(device_info), ctypes.byref(handle_ptr) + ) + if result != 0: # TRANSPORT_SUCCESS is 0 + print(f"[ERROR] Failed to create transport handle: {result}", flush=True) + return False + self._handle = handle_ptr.value + self._is_open = True + return True + + def close(self) -> None: + """ + Close the device connection and release resources. + + This method should be called explicitly before object destruction to ensure + clean shutdown of the C library resources. + """ + # CRITICAL: Ensure clean shutdown even if called multiple times + if not self._is_open and not self._handle: + return + + if self._handle: + try: + # Attempt clean shutdown via C library + _transport_lib.transport_destroy(self._handle) + except Exception as e: + # Log but don't raise - close() should be idempotent and safe + print(f"[WARNING] Failed to destroy transport: {e}", flush=True) + finally: + self._handle = None + self._is_open = False + + def read_(self, size: int) -> Optional[bytes]: + """ + Read data from the device with specified size. + + Args: + size: Number of bytes to read + + Returns: + bytes: Data read from device, or None if error occurred + """ + if not self._handle: + return None + + try: + # CRITICAL: Allocate buffer and prepare for C call + buffer = (c_uint8 * size)() + length = c_size_t(size) + + # Store handle locally to avoid attribute access during C call + handle = self._handle + + # CRITICAL FOR LINUX: Release GIL before blocking C call + # This prevents deadlocks when C library blocks on I/O + import threading + + gil_state = None + try: + # Call C function - ctypes should handle GIL automatically + # but we ensure thread safety by using local variables + result = _transport_lib.transport_read( + handle, + buffer, + ctypes.byref(length), + 100, # Use a 100ms timeout for polling to avoid long blocking + ) + finally: + # GIL is automatically reacquired by ctypes + pass + + # Check result: 0 means success, non-zero means error + if result == 0 and length.value > 0: + # CRITICAL: Use simple bytes() constructor for safer conversion + # ctypes.string_at can cause issues in multi-threaded environments on Linux + data_length = int(length.value) + # Create bytes directly from buffer slice + data_bytes = bytes(buffer[:data_length]) + return data_bytes + else: + # Timeout or no data is normal (when the device has no events); return None + return None + except Exception as e: + # Catch all possible exceptions to avoid thread crashes + import traceback + + print(f"read_ exception: {e}", flush=True) + traceback.print_exc() + return None + + # ========== Legacy Image Methods (DualDevice support) ========== + + def setBackgroundImg(self, buffer: bytes, size: int) -> None: + """ + Legacy method: Set background image from buffer. + + Args: + buffer: Image data buffer + size: Size of the buffer + """ + self.set_background_bitmap(buffer[:size]) + + def setBackgroundImgDualDevice(self, path) -> None: + """ + Legacy method: Set background image from file path (for dual device). + + Args: + path: Path to the image file (can be str, bytes, c_char_p, or os.PathLike) + """ + try: + # Convert c_char_p to string if needed + if isinstance(path, c_char_p): + path = ( + path.value.decode("utf-8") + if isinstance(path.value, bytes) + else path.value + ) + elif isinstance(path, bytes): + path = path.decode("utf-8") + + if path is None: + raise ValueError("Path cannot be None") + + with open(path, "rb") as f: + jpeg_data = f.read() + self.set_background_image_stream(jpeg_data) + except Exception as e: + raise RuntimeError(f"Failed to load image from {path}: {e}") + + def setBackgroundImgFrame(self, path, img_width, img_height) -> None: + """ + Legacy method: Set Temporary background image from file path (for dual device). + + Args: + path: Path to the image file (can be str, bytes, c_char_p, or os.PathLike) + img_width: Width of the image + img_height: Height of the image + """ + try: + # Convert c_char_p to string if needed + if isinstance(path, c_char_p): + path = ( + path.value.decode("utf-8") + if isinstance(path.value, bytes) + else path.value + ) + elif isinstance(path, bytes): + path = path.decode("utf-8") + + if path is None: + raise ValueError("Path cannot be None") + + with open(path, "rb") as f: + jpeg_data = f.read() + self.set_background_frame_stream(jpeg_data, img_width, img_height) + except Exception as e: + raise RuntimeError(f"Failed to load image from {path}: {e}") + + def setKeyImg(self, path, key: int) -> None: + """ + Legacy method: Set key image from file path. + + Args: + path: Path to the image file (can be str, bytes, c_char_p, or os.PathLike) + key: Key index + """ + try: + # Convert c_char_p to string if needed + if isinstance(path, c_char_p): + path = ( + path.value.decode("utf-8") + if isinstance(path.value, bytes) + else path.value + ) + elif isinstance(path, bytes): + path = path.decode("utf-8") + + if path is None: + raise ValueError("Path cannot be None") + + with open(path, "rb") as f: + jpeg_data = f.read() + res = self.set_key_image_stream(jpeg_data, key) + return res + except Exception as e: + raise RuntimeError(f"Failed to load image from {path}: {e}") + + def setKeyImgDualDevice(self, path, key: int) -> None: + """ + Legacy method: Set key image from file path (for dual device). + + Args: + path: Path to the image file (can be str, bytes, c_char_p, or os.PathLike) + key: Key index + """ + return self.setKeyImg(path, key) + + def setKeyImgDataDualDevice(self, data: bytes, key: int) -> None: + """ + Legacy method: Set key image from data buffer (for dual device). + + Args: + data: Image data as bytes + key: Key index + """ + self.set_key_image_stream(data, key) + + def setBrightness(self, percent: int) -> None: + """ + Legacy method: Set brightness. + + Args: + percent: Brightness percentage (0-100) + """ + self.set_key_brightness(percent) + + def disconnected(self) -> None: + """Legacy method: Notify device of disconnection.""" + self.notify_disconnected() + + def refresh(self) -> None: + """Legacy method: Refresh the display.""" + self.refresh_screen() diff --git a/StreamDock/Transport/TransportDLL/libtransport.so b/StreamDock/Transport/TransportDLL/libtransport.so new file mode 100644 index 0000000000000000000000000000000000000000..8246188f9385065d5fcdfcb0d64c416b2db0ae79 GIT binary patch literal 207736 zcmeEvd0Z7$|No$(xL!<6TP#d0%``5GN@anHUJ?~W%Sr=;tB`EM#pOv81B&|)J+xRp z&(nf!nq_H=nr)a_wpeOrmYJBDLu4+MOJ)AvpL5O(pL@6$eX8&8ug2Fn^M21cpYuMS zv(A~h!~D2};Z1^qni^_rYFupu;k;;*Bw`dlsJk7+h%p8kZSa3LBTR&Cro#9;ta)6r z*3k%+n9>NQ(-jiy#|=>w!Z55Ok0;VQB(EFV@sdJm&pPsW&2h<<*+c|z&G9`>YA54m zIONfaN1-TQRhA52HP`nTCZS;+LsWFqNPCUCt{=lxIP0j#Gc0#&$8Z(TI#L?6wZKm2 zf$~RTXiHFTv*IP~^Q65WA^^5>au zs=65CRCyTsB(3;Db1ZVQ-Dy7}nQ_4mhrz3TYPO z+FG`)=hACKf{d^*qiC4n4ec4UBII_QZ>?O`y{i!u78ZVJW>(cXt&RTO5}FtxvF)4e zukzk|R>-*V+e%|@ersBX%^lzrOBd{$J##dVK-ONi=pi%c6lqf$y%e! z#XYM+t`BMHC<>~MPl^jcjF6WnjZEx9E+Bs_!v0?Z*dD_E2=*%M`u3P2o>Ir9I6jSi z8TMzfKZl*Vf|b}`!2Tlkm$1{eN*;{W>bM5Swb)-(_;q!BLmlbxCiZIV>#+0fZ3Z^r z{2lBY755>#M%~}X*f(R}f_*D?+CIU)4f}Qi*gnJlg*Y>eojC5oz8m`<#eI$AUhMm@ zf2+9t>S*Hl9rilx2Nd@MjtABGVH|(N{uB0}u^+{5ZO7Ef33dDh$KTZX?{o}O0EZ^( zJQzo+$1N3ZqmJ#=(Sc)o>}O%`fV~s;&e&<|qK@a_cpmoiv0s3_8}@MQJ+NPl{Zj0g zVYjwEIPc5w{nxt!9El_ZNqVNsdK_sUp5-t80?AI zldzA+em(XZu-}Z`+NR*#jeRQPrr|t8;ak)(OC7UuoPoUnyGL;|ah#3)AJ}ijJ{S8u z?DMfNz+Q~K1bZp=JFwGMhNBnzKe3l%ufTpc_Dbxu-K&mtexEwOUmYJ{*B0UYzt|tf z{wVe;?E1DC++)}u$Nq%6{uGW&u|KUa#aV{)XBA$~_~&r`yuvGSd;$B5*k8iF3j1p8 z*0zS7uf_Rm*k8x~;IZ4AjlSaI=4UPKd(D!kk2HJm`ki}MwTVCZ;?f6>ecAcbisbEC zcl={u=}X!K72Cu(i7uaR3v>L|NcY&xZ>ld&%I>ofZN+$*MG%>r6t3&%#(9IeZJu8iU0g| zclq*Lf69Ac!WE+r^e9@g{F$}S2KWB-=$p0YMJ7!d^6Y>&A6-%wHF8?X`>zeW!*$z% z=C?j}v}N7quddIT-8239%VICOE@|Y^X~Tbuyx_yR!M!iZ`e*iazg4!LR=T8r;%g)K z51(-Al~z4Rd@%E$zeIP6KlybJ^NnYAd=-B9_&@J^t^a33cHhwLa9ZjsnfD!vfB&o7 zuK4&@=e@xn&Iwtz`k9oo+xI`~><(wgpV)cbq{K_UDPMlq`U(9HWOv-OZqYew&gwGs z-JmCylnq-MbKvpmk97TN!rpDi78YH2=;6wqO@rUPxaYR7u3vQh$fJGUy!!boHhs11 z`5S+$y#Iz*l6Ed~T=(quPqNltaN^YuE_r%Mmt}c}_O|{yX~fP;kERS=a@BoB|Glex z(Sz3nzcuOho6q|Ct53SE`5>$I(DjeIU&##m$IH)LSbW9D?QRQM)@NgImr<`KhMhIG z$HLYFzgYLx#F?`Q$A(x86Vg;LX2P&Uv%VhX=p-BEi_zgR~yDvRj^4szB zF)<}gQhsRnVbzEwSM~n>$Ym?uxS>f{&!FG-zdhx)YjgU%@ZdGWqrb^~CG(dKBZj>5 z(FaehUip|~>Gy}O-}F=am1AxXS@!7O7bnlYvvcqh!w(e?o;Gc7`^WZuU$u8o*~O2v zYdU{hyOHLs4_t2!zQ5al+P{_Y!_>OOr+Sxt@{fv2S9IRmq&DBQ9vbt=Wj8l_xy|>N zU;TC5*V$PE(tiwk-}%k*v%8g!e(0tnE1RXx+;G+Cva;yMdwFNLnq`0b-|w>CIA?X6 zJGQO=Pm|Ffj;!gs`;)9*!=4?s=!CZuSgw!dtG+HBi&kG zT5$7=Wxu={{nMnSbE_*abZ;-3nA-V?e=i>N__7{zI^>@}ee?5o-uu#xJC}62w*QH@ zFE4)QoC9y~4-TtK`EY!naSuLwZ2xPr z#;w!xcW<5la6xyPq6FNAcEI@O=W6d5|NOHE>@WPB3;pxQA~DALv0r-Ytwq+y3l#8|pv&W8MAp+o8u_zFrTI-xfXmhyQGVe|}^D{mo{6*OH?VA98d+1#McAm#gi$D911gKXt!~BQuJ|b_{f4*0MdQuUx+J?{o6-@yUOr8+?U{Gz}A`co6Y&)&sqq_OG-!6bZ zpAMk&<^b*V-T>|H$^dr0=;>e2paAtV2=@KepLc`&+o=gqe~OT)+wvFwxj^;<%oElJuzx{-`WzIXAHF*P z?+o*=vwwhb`Njb0V!C9&RD&(*BLDU;3*b*rfckJU*uR}l0mi}2G5*7!jP=jg1enj< z9Kime0QR>A==Y8U@LO^KI|~E!lW?0~Ki?Rjf4D3__&QWhf9tYb@d;< z0O|Ux|5pac@9+TqLr1iiZboNg_CK1yBGy60|9B2^HQ8@xpcEeQ2zv2nif z_MI}kqK#F}WcX0E_-L)ccD{z@d+m&xbuy$n7*FFl6!DwylDv96WsJam7V(v8@vyH< z#<&w{3q7M`_~uQ89AnfF$=7@?`OcCy=8AgN&ZrKTd~0QA=S4DnRX@r1mj@%VmC{cO z$k@gyJ1Na2ACoKjmWp47gh)?>S~&h$*%^m#wjh=63R+OdW{Up};~Uf? z;fG%(5vlkkQPNH|Ev93ep!f<^-wxd^`6-HDsO(n_ko*(M|Iuel`_&yJ|8Eukya>rB zs`fZb@$JK8Kekyt_qO`6Ay>)pHMGEw&1x5yUn#>I>UlT1U$K3Q@rC@hS^3SPa6}uOd{vx<%QE4^aO{q2NTi^yC=ZSQUQkK&dnIS!sWi zDwlp}_*Cv;G-1PL^<%H2qac2Taxl6JvGrE%C{NWZWM6Epl+Ki0(oU@6C#m|AqSc>U zRlgLf`lUn_{#DFlsk|y5kam*cFWMHV{(Ob%&+{d1bVdI|;l)ELaF{lTZMqu2YP9o6{p9=+W;Remckl;Kq| z8;_tqQ+ZW3kp%*`i)}dkNp@n?xQXU0wnzjM>E0;qSkD?0pp(Y^9k)t5R{bA?0wp`u zs=VMSvE8Q1MQ_)as&*Zr+Vun#ey$qt^zrvO6+T59cP@&RIuk#UI^hW@rJZ|J_y{$g9#Ri#rzn1t z^5=@CGQ6cH5B)0HDVihsA{BnTYG2|3KRD$k<1^GB3Sar93~%}Qp}|srSheKa%7f8S z=_%6m+^qbNrz#Gbo7hgMdR6tC#FozARk>_#E)84!OjRH9w0ZA`LzI8ie1o3j)Aoq+ zo8Av^QR91x^1n5{_Y>=Tn1^ha22!ONqh)}0ou&9nt-Zg3_9gBkwDB}zkn;a?(mskw zY!g)fP^J3w_NY&^-KfG>s_?BSNE3uB*mAB`$5#JeUe8t5Zf5cE2#c#eo^w4{`Kce zeuWzE;^k%IV^tqgwEA#N7YVUu+H(G|dN!lpQO-iO8QwSL>fIP&&oSF0f$f zY^D5GspcoD_>GB5XPC0zU!@xhJ(O-}Q|U-FXR+0yp;5X;%8peo?Nz->QT4}~H>LL=~M zjbi%={WGOorp+5CS@W5uGCa(R?S94QDcW=J~b|<43~zx zsqp8idOK08x6kyD{;$4QhQC#X4_5tN${xwzulOsJe?ry7wxzsm%v9|$QQc>SsqnX< z;Zu3({VLvt6?~26pRdtTi~0N`((pB^{`6DrrRo{UPgVBMik5b&o|n8DX2i0FsD~$H zK+FG|Rez%QdlQuY2&MllW#6sJQM_OX?xgBJ&r|(FjcP|!uCz@-y`}oLM!U~CY}uJF zby|Kqr0PSJ*6(#v<5$W686NV)_OdlkQ}fk_760TA>4)l_GW=VLZ=&+6_n*B~`5Ir# z@K*W0&|Q^_T4=P=J*?{CA*~*ELHUrMBh-tXR(*ECKU9uYt)%^H(y-nlu?ub#umQ)$UfPc4zh5?fS{~7#1WQpQP+e>@DqV9x3fu?d6oJZ-=yYeVb~( zMeSwy9!lrAs$6PLNW)hB>4pA^bXLDA1D>bCzozsQDLvzrp2t+a!hZ9$bD`>QYczf` z#tZUW3N18Y>!$3_Q|)4fY8M!8#I{t8Uny$*vi$jq(pmGGG;mynKcebS=oON;#;ch1?8Pw6O$ZHnKn`WZvb|2Hc887g04x5$9U6u(-Pdyy*lt&0B?HyWbc z>E%{z-zfW+!{IbuSE+H_nr{qK^{qy0FHd10BfrI{{u9kjY#vq55ABtPt?~Cp3_xN$ zQuVo~3jgkvO3zC&yw#t$Mfr9%QdD@jU2M6ko)oEyKUC@Os>Vls9{5O@v>$ev3}}s0 zajIO3RC~AT`P;}i`KRb28Qvt2BqpELtT77#Oej|VO)W)O7RDIZ-E7RSj`p;+8yr}9b$=@dr#%fg$ zYqa_>0cneNrz+HkLJi)DSM_jnsWf2qtN&0s_4TV;RQ;(;mf@}bYSIvC-%#@kt6XxT zWxiHu^M}_}x-nXLwY17h&F9<5?#cKdKs!2czD&335~=5Qm9NK@f2u#1Jh~~deb-m& zF}3j}M9r7<`_Nu$oXS)4SjZOJ{mKtvze;?C;y0@BMJjw(rN0vClK=Jl#64F3HcjfZ z+WTdSuTu7{{?kZKo|c`PlU(3Q&G#fH8_DtGMt7@r%uUo z%WM9xB&W?zO`e*WlbV(J54YjTPt7UF%Z0(Te0Qn`F>g%r49v;(WaQ`00?3(_J#J)@ zCt5Md9&n?h$6K+6I^)KTbVg-lrbEfh%rtj$X3o@H-!Q|RaWlcBXC=+ zFUTbYo;k@gBh*Db9U*iIa+i~wmg-5%U|I{@o@6L=r=};n^Ye4_VU}{`&Pi86lPBcN z$^;tcE-1{VOpPAt9AvAGo1Ny)^JM1cI3uUIJ#l1VXl}Y2GV9%u?k@1;=gwgag2He4 z*|Spf-N`eN0~qklsdFI03|B#}b5npJQE9VhJDqXaMz%XUJ9j2&Ax}Ut$}w$va$3gp z?Tm!2pT6t zo`KDRJhwaDS7~H_KNgcHTwr_o)27t>Kr`2mnHx7^h@JPij0~P;Cw=*XBMR8=H#Gq=w<1Q zXm;pnP)p;}k~~glS_ZZK{M1ZOL3~uZc<4Y$j)>Wq=6mUk$fO9?o*3PTGB`Rg+hB^MztQOD-fU#J*) zsJs4SB;txs43d`POfHy?xrE=g8if#}ok_U5OlF`V$qGn30&xjZ$Xv-85&5ALbk0Nd+`cUGi~&|L}t>+DAvd{Un+p`c?rN zy<`e>F%I>aHLU#9S;;gN%t|frSQ9g!3KWG9uFUiVTo<#hxWrkBykA1^Ow-ro+(J)Y zq1F>S`>CsA#KpKNEMZF2or9Sk<}Q3rOFYIFw|!AfY?3?!W@32A&5zGO51SDe=NyQx z65h_r%1y%*w!jVNUOR3iuEfn89gRtAPNC?s(R1hI=4Yp7C8P9^7%U`aB;j|Qal`V) zjK`duCd~Lh$rBloOjFd{S!j$o(>xi{ABiK=m~8TNx|}v+z?zylO>;KY%3A_^(FE1zPVg?QO&CVppkB|2^Ultc| zH(;k@5pHWnP3hu(Au7!?C(n(sYfyS_@~q5scTQXyU4mcwB`4)2r(xK{=$xGA$%ni1 zvQpFB@y-GE1U(7!MHCgvCMhC0E`gUoWO70-rpqP%C%eLkL@(#)AOiuEkTrt?^WyOz9N*V>M%W}tGJF21c zhtHfDmzdNKRU6v?Y956-|7V_dMj!!o%XJ2c{2|Z$|43t4#2KjngZ11t{Z|&OYCYqx zL(}{#?Z(O0u1pS)Rj|IP{*_6o^@YDU17}o{f20wj)#y^GzqA%l zsGoF3Q$LxF`}yPo+!CO#^rTLUN={1lWX8>mi$af|O2ftYqyRw;%<9v!#XZUYL8t3H zT^?w-%gIf~a4J_5a#C}0t-Cj&*cu#jr{M-jEaK4E2+wINr`$3`dRW);XAXlMN*+K@ROx3oAklG-25 z+(!GqPAi;^Tu)7E{5tH9X(T%4WQ|>h^~Ns4IJKHav^RQL#1=RHfwGS1ja(rtdJ*Vh%{Tdj6RR#(n7Ta;}MCCTpZ^$TjcAjTWBc`InYD;5kGiM{6fC;EhsxbqD zJh@m*!or{Rj6h_}zuxGq+-b7EYDBX$q97x4s%IRnddFpp^;Ex^ADD;xempC|3PC{q z(FKK5RDxJJ3>eHkL-u}nxS=K+{`Kd$8}B8ZbgL_G?1$6+wI>cYD$YnBhE({?k24ZO zE(UcKGjO;l%({F`lERS9h`@&3N5N_zxGwDG} z^32ryOj^~bzy1DSJ4|usf-u0_2gN!o;(Yy9dx(F z?TIxD^i2L_ThnW2gZu}4_SYb2|6J5J)LdGRq0{8NRIG#gE|V0@!Bfm^ITaM^-3_du zP39I*`zJHv+;6#9)2IgVt(-JEAJ%huMz{0w))JLP>J!aHH#8u#V2tzv)nolC9x|ap z`kCH{%vx)RxM82w*tFPBiJ$4TxWU}npDi(y5{qr}sjIkA(dOL#;)y9YJx-=Nb_U`3 zv|KDV(NhwvWySk5uDWVij!cfndKY$D@9u0opNQ*7@q@H9nGNDJOaLXT?^4tQ;VkI0v`|g>ld66cqbrp>YQY}bReCC&d|OqkzJ&-e`W!B zdYp8+@_55=(X?VLR)l@V!vDQzqkDkVJTbT>jzq6Eiw34V@f7lp8Ix~Ch{tDCkWM>; ziCL+hsTiszcrvq`1Mr+ae@^|nc!6liG#-mPcdT;8H}H*EAqi+O4IU|gcU_t3!Y?ow zc;@J*RmhHBq=8$e00U0Ic>VEy2p&AZgJSFtpt8XPCNjcyk1T&7om#-ihU<&`AqYY= z3?LrX(!gmwDnTg!LzG<=vG$Fsm?sxL>)&WWG;6^r3Kaz11KxL$fv}|Rj3~^>L;=!b zG_8-S#VWeeKixewwJ^(Cs)-jX{fV?PN|~~)_P-@JQ?8BDLXJB>8*kTm+;X1UV6iVc zEu%1Jx_EjLAOtnRG{Pi;= zD?-4UM2B2x*H`P@2A#@9Me2WKGm@wE}U@;S4f+tf_Fjv-e%;`pNxwDJHCr|=F};M)P~MyA1?oOmc^wE)$t%GCh9pO+6Rvqf2A znNB>$Y|!C3Bk7Vo6yTXA%_NA!nx_R-&5>2*&D5YFZI2%@fQYRk3(rC+`;5t#EQq$?pi1F$suF(!UV7*lR zi2a-n3p&&(jK9uj*5fBSMI1k#x+uBgnK@N1kotRxL?wc!cGfh?pTYiOp)(OCqhvs7 za7y7+*u)b~CF_4k(3%6?+9M^kF+)q&IctdW^w zQf8FLos~6BRj^FlkGiK%i_1bna#ooHoSrG(h)Bv*59ZToMVZR#Otd2?4PR>Z+n7)6 z`y3e~GMOIXVggKY(1)tq7?G}@qH5@4neqUdnP$1`Z-VSfSKphTiMo0NXH*sHgKO)y zVu_t5In#2IQwwLupROkxnFjX-n8^64&vLjC4=-nqPfm)%6R5}tOxW=DX1e|GUOhzd zzpNk@_x?zls-&IC!)TBZ&)-i!(umHMnqa1QF&9sM@@RcY+*Rbe)g<0u_lPb7)#~(9 z&4wK-+TIz(tRMH0mj5LWs*Hd2Gji3o8Zq0TzoudPv@Z-$@icUX)d>7tu}Ad3TuQ@r z1B_N@I5lxbVhxj4gv5)8@z;v^3Wh&aJM}7xpIi>|(3{(-X`Xn$>s@hiGw~KnHeMW= zT9}hYt5@;EXHN06DxqFUWKVc}9|56iHFm=ixVH3Iu|C;IdSAQ#<-7a6iv-!7uopan4(}Vhqqb&rzqErMjNA7 zw^FBM&UDh7QsT}!*?!zE<~8&X{ePXUTY~;bJYQAxs#rm$e_;V@x^Q8mt3Plti|yM! zMeO<;*uUf*A>m2fK->4)f5nSOx>4)qTx8(?X3Rk?Yow#sZ+*^m$f5e)h}l0plu@Jj z>+M=gDEYJK9xAyohYt3_iehKw`(yLd1)GfJ6!{AsdeL0{xX7aD;fT2DOQ(A_+pf4z z(-V`Vw26V+&#!pwq1)dz>t%)h_R`cScXn?6oQCf^1P-BW^s8U}8=9M)jhjsQa!UhK zrw`arm0B>f)@H3dMb@j*Lns@5Jz~9J5jeIAaeAo@P3aC4A(CengZQD+4U@^T%^3ZX&L0#Iz{EK=&Abw$l4l+6FT(UTL+y^p9uQ zx^d!_Rx_?uTJ7qeF39mo{(4g+ig{>b4G9l18@-%#+B!&S#Gg?pu0+efFv65HyzH|~ zBwk0PTQzH><~P*!7p+0M{!Xg?CJ&1?c+{HaAuIM;&$L2Xpi;MXYW+uY>bWeHqTjUC z>Za(2nv|96v7d+x!UG8W@&)fz6x!I3r@g4XgM*Klw?r?PBHd zw7>kZa#+9M;s@d)GYcTdvdmT9xtYdn521frzeZq8s5Ob#ms`-wCL#Mf&%sP%02TP* zT5h)A(kaZrZ=?O&anJVX$+OV%6hEDkZWE8*q%+BLa1x#$=1iemExeT|Tt!dblBeQV z(S`Wt0p1wHj}DE@+%%6gKsnPcN<)YwU&*uL5KS+BQB9;51qoAZm0dX}Xer`G*GFz7|8gommu_wbCX21GHy!YUq zjUQW}`dB_7oigi?i!Uo==HX|520oNPRj6-4ZeJ$>b!$sFf^dN+J*{_df&&OnOT*8+ z?Z9b@Pw$hJnNv8s&+I`1`V8n#WF!T)Z(G0^BZdx5?gyJA65@vpO^)o_&%nuqVR6?b zI|ucvXA#p15!pA&KJRaz4Ybdk^<=-k{f(g$C&mtmPj>e0*Ed>*nK%(C_V4R-vTHt9 ze8b@nNr|LYv`D4Ivf>jM-%4qMzgXN1f5o^t&g8$Q;@A{_`MHIkx6J1-|an z64qOY&@w!QA#1@RwA4fCORZ#`t~SMAevsD4Vr!9BGhC5Z&k`zIgC}kDIhYnk2gIR# zk}fMpWSy?coll8!Iz6V7{|cTM%TBp z>m4#P@yWEaj8p9T-RyezAAiHA)6nLIS@D|~|5exV=}II2oJ@Q=slCw&*D1}ya|t&y zy4m4_jg2aN3a)n)>4h47RQS#Uw=)JReE&TUG&j()V-@xY`)!N_g{A$b%zlXRjIs~& z#+)tBBfkz~q6(jS{6RFl)`nZ*PoAHSFK@LnJPLc~_rmp-#(afmi100p3Wf6pZf-oJ zaHhb)#xn|gdff~EG%?l!)8H`@zsh?(fB-NCr{(MsPC@L z*7%|}^1D+-8egUOGL7G?_%LP9(qD8^+8Lzr#_y7MX?&65Cu+Q*zOR;{{@#pb-@?}S zPsu7i=_l$Ec_>2J(fODFd}088k;*UGvA(fK_O0*!6Mr3cs>8Hd->KC3&CCuDZ+&-6 z4{v>6UgxcH(|PMVdOAOw<%_3leHU8~Z+*8;=dJIY>b&*cUY%dc@@sv=kNmTO@#86y z+Nv3^K4B-WY-D^G3ttm}KOTTjRNtE;6WmTl0KO;?ufE5p+sR{p4~g5~!Sq<)?4*nx zV*CRvUm>i#J`*00|AjJMeL7HF=ox^|VEpaOejek$VfM=c@I{O-V|ErXUVYM2SgT^Z z`jn;Mmok12OLqn1cQU@3@ei?dH!}Wg#&2f)0cK|h=>+`yv6ts#y`aPP{!ZI((S_dH7tA>Gt62EM!lL|dDdPt+JDUUW#{=+T ztX*?Et^j;q0De&beho|aNv5-!@#+(@B4ZmFZ+(N4F4Qod{w9>QnT+3HkvJY={F96i zVLIPud?G6^&WEz_n^^coEIjACu<*~a@DYqxpUM|kCo(&nAH>2RWOkOa@SKlf;oo85 zCo+Bn^IHnzcd_spjDMB!WsHwyb}AX~V0^Wz&({3FGPRl2b7~vbwuAB3w?*li$@m+X z{X>k$CxPwlIOFHC@CNhS6vl@z{vz87{vIIXFJ^ogkPV~k(K(jCNj zd>Y^0Vi-TfW-(7@{1ePh3giF7_zcD`WqcmvQyD*-@#>SjBK0E1XEHryj9AY;RSJZ)3BDv6S&`8NY(@_!ABGwubQzn?*fm{5+;} zBjYO>znSs8&)vaz>s!>6Obz2_F*_#X@u_cnJH+@4Soq_N?_h_<$%6WN+mZ1hjJLi; zO_xI%e>Mx>h4C*jK8*358Q+uf*0;&&as=b+SolGV$0xV#Er#*u*(~~D#&2ePBI8#x zej?-XiEevKVf-UDYZw`fSAU8{T+L(rt<3+k8NZv^DPsH#7QT$}ds+BO#&>1>BF109 z_$tQtVfL3Y{&5z51>^B)c6(dH_%NGA{b&4o#&2Z2`eeGex|#7`vhX_?zl!lSjPJ&H zlkuHdz78?|b;ciOd=;}}++JV*FJ|FG7$42}P{wy>cDgWrISU`g_;41!C*ylCK7#S; zll9{2AjbD(;bRzo8%x*4_}3Vp$oO8&&P2wyW#Lm8e<=%}!T8>c&tv>$jGxW;cNt&A zczkl+-pUw%xy|D5S~5O@@rxL5GQNuOPqBP0WxSJxU%`0wiFKY;O} zj33DOE{q?<_%Oyl#?tM{_#G^K1mp3i80>8jqi~gVSv5e1S{FN-fvl%~>g)d_K7mP1s{2FGblJRP3Uc_3&_&8># zit)o4zm)MK7{7w?F2=86d{3sQn(<$;@EaK)&-l%ZPh)m=F#cK=zJ~GYPa=w|CgY!C z;SVu>B;$`WK7sK@Nqzkv#rP1$uV8#AP#wRjC89t)qr_-QPB9^)sn@Ut18 z!NM0Y{uaiUF@8GZD;b}~_(hD*W_%Un)t{6SSC=w=8w_jkrHVZ$9@pBj-!}xzN-o^M^8K21bmsz?K8UH*BpThXLjL%^F zJjUlSKA+j2&G;b37cqW5vs1?SBF0xTegWebG5&VOS24br@k<%so9S7>_!1U=4dcUE z_-e+NvhW)je+T0?GyYD-?_hix<7*i2Vdta z$oTtN_!P!3Wc=T!e|z9>5B%+czdi7`2mbcJ-yZne1AlwqZx8(Kf&bM5KRPb>#Z~-$ zh^svKy^n(oS827U$*Ik*;ulUq;FUIRCh%e#2Mc_UjhhR+z{YfQc%*iQjY9-ZwQ);<$J)4+z_B)N zEwIzZ^diQQ+HN+c7ch?0wzqLRfrD)95cu%#R(?YT-ecqT0&lS~y%2%?+qi?kD{b6S z;KerXB=9{prWYHI)Gn}bXMtzfxQoE4Ha{pNgur`j z>=by5jp;!w%HPKQ1YT+5{sJ$yag@OK*q9!8qWo=44?0o*Z9GWeR2xSNJl4in2pnr; zdN7IdxA9d1ceC+ef!o`d9zde}ZG4Tuhkv#5A0zM{8^;Q~#l}MfUT5Q>0Wz6+xS|6V{JTAV5g1g0UXNT#`NF~?cc_u1rDee2ve;a2CJl4jy2pntU=>j`# zOb;%Qe;d;S3zWZ&a|8~uajw9Jk6Zbt2Lvd88_y7Ui;ZdFALVc30)bcB*dy>_8y5R%hdj#HM<9h{OXXE<> zUTNd|1zv39g#zDW;|By@VB-e`o?+uf0;k&e-vW=d@qYx4wef!icG~zMfxFrGVS(G* z_z{7FZ2YLehkv&6UnTG!8!r}ki;W)>c%6+O7kH(OpAdMljh__w9veR;@B$kz5qO4; zmkOL}+#Wvm`@I5wuN8klE zeplcbHr^<3s*T?hc&v@z7dY0&9|-KU@rMF;v+*W@+uQgffrD)PvA~CqSoz;9@E#j) z5qOJ@w+g(@#-9kh(#G2aUTot}1-{3|+XY@=ShCkKtCJk@U@ar0WS;NaU{G^5-*6>0NS7^9Y!}By;sNrl4yEQyn!xJ<- zO2cs)9<1Rg4foM-4-I$Ka3>A7)o`$ePrm6}zkb&60S)id@GcF1s^Lu<-k{;vHT<%M zmuvV*4L_{mg&MBVaH)ppX}D0s*&23hc(R5kXn2%{<1{>2!%-UUqv0MJ?yBKV8g8rM zU=5#qLo0s`AJFhV4e!$MryAa*;SCyoUBfSHc)5n3)bPU^UZ~*;4VP+oo`wrGoULKE zh9_%yf`&(FI8MWZH5{ekJ{sBu!c|KH!eQqui*n4-lySR8vaznn>4&Z!>?=jWeqRa@RJ&TSi=i7T%qAo z4bRhXp@y?H?AGvP4NuVUC=JJHc(8_}G~7qSJv7`^!<{tTR>Q#>KKZIv{u(}@;e8t3 zrQuICyh+0wH2k`TU)Jz)4L_;jhc&!V!xb7X)$lwG7iu_L!)^^v*6;)kkJ50Qh6ign zO2d6L+(W}%HQY(VZ8aRM;gf5%^4IVI4e!(NE)9RG;Y}LepyAgw{IZ6ZYxqeGKdj+} z8m`cAsfOoixKP8{8g^@VvW6#Uc$9|YG(1?tQ5x=};T{_9s^Lx=ZmZ#74WE2PD}N0i z(C|JByUL@+;yVNh-u))6Z@G$pZR#03zax%8juKiZ$Z39V?kya&xuSd&=W7l4*z zPPr=Dj>1dzu8OEA9Ly9f*wkH0!7fHHm-k&)xp68nbZu{nToKhCVPrJ#Ibd9-C)iEOGXys`ImS+49=~%3UG971J(wRfH@evcUX8Ea~Mv_aZ1M3T9@f#vc(u#}TmA<@d^e8nvR z=OcDImKN61D!(OmdzcWr2M4njG9dN^gdvw)3L`=+J#P|XN0He3)>L4f&p_3B1y5!5X^9T}^Iu2=o}DPwn49rHMu`0kCqiriiH-IVOKbC}?Dr$S?b#6|_Ax|~Vh1y^cY!6( zz661m*x8D!BGKHlA0LERTD-QzetdxtJA%Z14H*#o48l+~=nf-7?4>vnVq1~eyX0l% zSz0)U*cM#ue_+S_8P8s%*lj)QJ-Zt$iEY8eexSG?Ni-LG5s9UhWlQWuQtbOw4Wgvj z-+z!|XTpdOI}ay9?88Xb+$}FFv9ziUvH#@N-~bXaU5F&bPGe#tm9QHj%<}97iW{zr zEksIdY1!8jTi8{2Hjc!WArW|XtP=Ykp34Zan{gtz7SiDs%!3q42XRKVW=9M4vC$;AUk%osE5x2pVz)sC z#6FBLR1G@8h!A@|PK4OmB=$CWS$USeZv)SM2{X3XyI{xMkHK1s9m2$J0ZU>}z@#Pi zb;a!?(Ohgt5({Cq3@*F3>Xn&b8#ZXI!Npmd0C0=BE`D6*xg9P zyaJJ=*w1^^dp2ANOJHI{6*ov1I}ItVrC;kvH&nEpc8>7uK^)9~AQ5LYfN6uSpzY_Yvb>>Y?C#dc$2^T3j47eS!q*-45kA<^Y?!^2$B zyFI~X57Fx!Oz_sYijRv)?s1yrKC&MLn0>4FJumSv(DsFrFIANi}L{112|xR~TGKpZLmCtLn{b2?avGVg8a|A9zS>_tp$x)OF5L|dLsRNQRJ2>0xg zuOaq#CU!}CA$BD+n`m1lpIVrL+}?b&?TF*hN$ z6uX9X8mnbrX6}YbOYGx{`+!7qvA^tr*o&Lg_Yc2>3b7ZE*tSyac7!3Zqb;!$a3VZg zN@7>b%Svo_*h2IC3VF80Za{X-Fhr7K$1t(2l(0x9_NeU2%nNj}qe$#PCU%q*dnXbz zQ;-Ne+e?Y1{!n;!DNcmgE+n>ng&g`xlWFVep;4X*o_LsZ1~+rGX5R?L?WN6N1s57(QI$R5^w2a=`y_2xen_X1^uSByQoA@xQkbq_uaih>8)HiA`*!(#4o&^*T$ zyTRP0xL{pue-cZ-6_;+PXxm?keHrdGM@X@uN^BX72(kadi4Z%D#2)@ittewDPdX2id8Wp6gLr^k7w^hN^2J}v3KH=Z%C{&iTw`}foIdC z8gnN`Xd(7%oCvXzBzBmO*o9K;XUMZHb{vUaj!07MZ>$zS2$qJ4HzCjxTdcTeNHnho zU+jX|rgOD5c?sLH6N6hm1m!lVq>}3Ct=4l z5J`%CfQkJMV>5|8i;3N?xZg=M7u$!#u4Q8TNU{6jU^7OFZK=dAfDs|K3@1YDJQDle zmm)7p>}n}?3F6zHJq`nA5+X^lBbeC1N|+nMEYJ2*+*n;~5mH+FITKsdT6i{z#NLNQ z;Mqx1jkyioi4eOJCqisEi5=`CcDoeYfr}kUVxK}JDfWkp>N~Sa(M25;^H)pkT*W;> zqPb^3*$J_=OzbDP^+z>W4+opyLI%V>k1$jXE`bptwl7YE*cv2jR?5rDv)@Xw#fWcv z_7T`IPhb?5VwW+od%;pQXboYO*pC%=j6`E9AK@ycdE_E$=YXLSdI{org69f)mf*RL ztOQeTIbG3TzcWz|jXxpG~HF+tOpkPpkr8gh_l*NF(TCE zZQ&|C>L|Hg&?jBR@14R0@3AA##e08p9oyonxB+s{4@agQGtY5(zjbaUjddc;I0F{L zUEa7*S2@n%ox+gVe|8Kn3$26Kc6wSPqY2(`UEa@sd_sgQpMbRELf&cC1Tmo@-PKPj z^0YTy>(*qfY|eB2mxy z=-+@Q33R0^dTw|K;3#EiAq-WzN)LEK65zvum>|#-tND!GZ*~*xK{J!sQ8Mg)vo%=q zdlIp)(3_|U-tXRs6vzGMCLxTfYQ-?Rmwp#%eok$=ywfiapna7`J%aqtI_I8!eiiH zv%_{u>!5in8G)2A*Oha_yTAwMDltQZn1kkx2w*NmXfq5#YOlH(A5nD`ze+uvQ8gsPZw-+(y9p3w>Rge4tl3*+QPjO>hAZpO$~awQoR2=|7OH{B{tj{gBr3yC zQy>_gfv9z8qd5ImhD`99u3|H&&|&U_c{2}AsdZ!^v)c)#5;OoQ#!JffTw!|{b-mS| zwriV`!@NHr#Hr1rO23h{aM2D*-)ft`h~#&hL(U-P%|0%dt~kTkPx{1ep<<8m6C1rO%80xrI+tVSEk02{q)Z^s z9T7EvYCC3xPt1E&%rDUOzB!x9r6Mal#@x9LhAN^~(s<`R81H@CeDDOS39f~KUIm&e zT`{;<=w}-)?{^8_AE*|cMHOCDbhN_=*$(>(7talkp|+VQxpM{AY2>}4`}*k~*?nyj z{VE0WQ%_XR^F)(kUBJhbA*kt$EXeV3Jt^R+J&Pcu?kCY6cAMWL9Tl{0Kg;$vpNMR) zZE0t_>Ue#&e{LePeHXY_sAbk?`@h1e)V}MpJy3C6NO!??x~o!T`(+$R)-H62R<9lA z@~&q_jwpX}BVU1|Xcw#8T)=YkiIuId{26qVeEmDx=)*8jy8~yZt1Hb!3e4AO!ubcy zXP`Jx`><=;J5-5yXWc`^&Tcfe>JQ5F*!zW;gXZOD68l;`_R}i%VRXEKm8-Pd=&`4( z*e{+*?2o7x@qG7Du?uDFlJF|3Ja!cqO$S{e)mqeinw{8ea~7G! z89Gr4vO{RxZHB?9tGwRkM$x@mHtSvyeJW~YsBCzPMYMxvE&6r%p+25(x6HKUN-cNZ z&9u~Q_0f``wA?SonR=U2i`CVWmbu}1QvNvCKs@P9kv152emZOwCxW2tvW+_ybc@Gsg z+lq^-9B@8RMsd0B{Xz=be!Pz3y0JP^;4(kO_X;x zw~3(hFnZ&b13gWS#OOfe?as|2bb=1&Ds9P7ad+L{B)YUQ{_EN>>3l?_e!<-I^ zzR8#VO?dI3In0;dr|3Su^hb(5ThMfuaSS@vD3^uFn~tXy%5(XOt|(q#WF) z=y#~ww%H8Dt`Mx9gRzSKw=aE#qHhNsZ_jGO!P*UT$tH5&K{FXaD(#AUk7DD&PIX*Z zf{{Y4#nAU>Dh5Yls$`xoI;g>uy_iN6zYV(6TQsAni0X)|wK*RlYF{g^vs^ivYp@p+od>Pbb{)JWz&nG~kw}Y8)39kvtEP)~);D8rQnxO9fyU)`GvbSSYG^5|eL?->zsZ;L+cSX3M$w=EypW@ff!bPP=%!Nu%IZ7Uf zNU>bv{gGBour%UHa=qJ}w7ZJ0K?)~<%cH)z1FJD~UuRCC^7Fn;*~}B;*t=%(d!TT8 z8)|q4#ur}|Zg?URyxp;?Vl8M)#hu&Rq4Y*)kXYbQ7_&zUQ(rV2UVekpAW)7{U(+=OKOyb{wN@e9iOFskszJL7}PH?Px&#`K&or_|hl2G;0 z%EuS6%>&o>fZ`2OJ!>f^F@Wa&O1GqTgB zpUG@!`n`?B7wLCpdH0k4(bL6m+j#v;!z?|Je%XFRA3~j&-}=A#bn&w>%vrwoqkk~& zSQ@4ud!5}wCHnMGE|{Vo>QVGc@v4WSccJ6G+o*?1b5%^jJ&|{Oco%er2^AjPgkya! z49_JTvu4uM2}db?%EGa#c^S?8ygL%S>ES)su4zqO?@{zyL*8i?W|%H?>omnu?<&m^ zeL!6+sh|2I>blT(T3uHP{F5lVuGpK`%X%#PsqK|6sOaL{N{g$D;Y(VNhba8gU>J#n z3fX6x9u&0bQ0YooZJHRd&~IZ_RrjTNHo5>+=Um>+wHx7f2{$0?wNC(AV?5EZ*E0`J z^y|Tgh1ZUXKAyhDqCA-D<-gcu3|MO1t7HDddLo960RB5__D3hj{FmwLzwJn#`R`g^ z|6Tllzc|1cHVXVpP^9F;MwyfCyp4)uLr8ET^}b(eSYM(0k^oOg%{C?_2BsS#Nj z9VLr|o2nBk=B;pfCoGC}tp3~^SAxSk2^ED^-nb&1R(nh5lGfGhyrr{olHi?JMeW^j zr~EMxwfSmcr+Hc2iUjZR1h0E3G=&T+A5|3V-62|hd0YwFe|ZI2Dql!u67gb2nfd0M zREg(RO}tUw4&=f~D25QsFU7pXRX%wmx&c*<(RkcDZlVcH?9H*syuYm5ZqHv_6@`mr zcW^KDGTyi<(Hq1q!YSm@{K?Ef;U|>47vWJ`Rc!*)AU!;ls;e>Ep&qbu2)4^%eY|&j z?bjI1#8uz%^A#jy^}*WsliSGnVUNG(tP2?b%YgAOQRBa3_0c%Ts%kNqREt_{4JLBF z<~yH>MoVls9?1ECF!8!50n3CvuT?&&eX5c8c7C3&4w#<~e#awiKJxh+0pl0>jo+X> zP(z?L!Mf(SjcRP8Z{JvB+q>eN#x_R@jdQV%RjB$_Ygi>)!zwfo(Hb19&<3aq*SCSY z5lMDA^a&#x_w+VUf;NDbK-FC|1!@OzUCI}doka6F^qaf^a)PH)MZq@&Rcn~h`?C_-PsOKEBX|fupQI)!gIta!dch+0=>3sj?3DcjTI`(AchvA$G@TlYdw{QdRxW78&t->^_ zI5)eBuQWWfUAU+8OreK_p(qL55RDHH^=>jxBCD=)oFlTg0}8s(QF>ralcq-S+9pkm zW{%Ns(OB7~zLOaM!99gm)_&@~u=^Kz>-fG84ys#gZbL#=u6~4{cz>TD%*6*o) z{Xy|{e}98Mtjx#RQg7q_rbTuC?H~6OtKW01-irQ2bOa@=5jQM1dLu*5!(@Fxv8&&SRCrSfL6l4acTkL;g(>wEL{zBtIPFQX{k`ituOzruP< z$*CB}!gtXNoiYb9@m8U{iKr?(^5|H7sYI&r^V+hQ$UE24@+!P)H2|NNMq6XUZ{_bvj2!Mzq!1hnXkP@ZO?;e1gMo5vDYW`Izn;( zH56e+p?;Uf;bf}Ik+c5VivGQ6k{uc!bYQ9@3l$g>Zeg*mw_X1ntK!fDZ3rrBDyO^^ z6N6|@)FnRpfTz0)31O1`RpIC2`Pnh^BeYTqEh;^N6z(FqFmE}?$XQAY1a z(86OdWV?dalP)YQkbLhtN+(8GB6TDz!qw|XPdk_Q3yituy(k0<74$jH(J>F-@A6_r z!fb_Fz!q3mo6 zzE?}LFY_W~$?UTVsaFhMvL6dji&tAsSgZhDiMApuj}2A*alwmXn;(09y!RNZE?r#Z zvqBQe2VxN}q1Pc-^hQU?@9-cdTnXM^=$?rRyV_AgA2ds-xXeh1HXV1+dQiM~6CU%@ ztrgzu=pSw_M2&Qn_YbF90%m=D&;|@n&B7CU{hm;f&>;w~6qPmW5MR;E>GI;eq0+7M z`=WE4KLa(W@M=+e61>@=j|dQbxHxEDt)uiN{2%8$;M^&`e68Axm{R?o;QiP<3vrOVXF=WT@KY>4 zNMabZmmm)Od$kGw#=_U4y{%_Wm^@6cwK$6>zi<$Rs^BTd>UbBQIH0n$+f-V zhRXUbiuBSPJ#4BYH`wvBVKf-iyn7wJnuXVe%Kp@B{lckJ#eq+=yJ@|76VhV>DLw9AtVEJbOvuf3FOtq5T;+Y{=Tq+9qJ_lE5S@O@ zK}*_2wba-Tnq9z&*U-vi%!KkMdLF`MS}#nBEt(rX$nbQJb-dvTA4)+7uS8I+i%&+2 zkDXNe(aYAjKufJPXzB6g&BEVkMfS^MB3xGnC(H;kJR5NJ)Mm#Uhw5H6cQ(}TSQT74 zC&(zN_5`huy?$*7VpAJSsOa+*>XFFN&yZUUfSJOt(1HAQF;hjyQa~?8xqA0ABcWw& zOPRAr5S03)s}UrjJPl2>->B$s3vaA_9oNOW!#l+_IE|oeqoEyxjrihs&`&0s=pj3W z;?+Uq03hhQ@*!P{YhEti7~&n$#e5x|h;W}+mwngVg+63d^iQ4)GK7iw)vof1h*Y;= zWEUgGFp6H?14BmPJ+AUs$*UMC9VHuZQZ#>%QP|z(or~u9G;B}@iErSyP+mJb51(@= ztwg|gipfHdx^0PpC^T6KjDDbd^@*aDT6xAv5zT-iquQmyy&@bTnI$U4d4auSoM13Py_1)-O ztZMwqw~`wRPCT67pU~^5*&Vgt3h^L9#M3(eyXIvG6fb-jV~#3+DOlF^qn^>RMYo}< zhrwwxhteB3NzcJ&h(PVJb~17++bi{!$5GSb%bW9R?na;G*@%m>n!jQGgmf%_#Ez%A z0lIL^q04hnUw?J=?(2GYL@4rPING=HOqrcdjjK|0EbFK{$1u7{&H<)}IuAJ7H;u>O z5Rav-5Ld55g=e{fu#hw|g!*`E-tGNZwD1jPD{9R&4%Y{PAD!>jEio@gJ>b0?cq%Vi z9gO$>CSG+7ap4i)NLTrFD676S<1fF4l11 z5&jxrRM7VcUN{6jrZ+8=icXcAJ^xfw^9{9&KOw(}{57KIQyL5L-s9H7cFY>`fkrTU z7$(SQ73Lz6fi+yJ72lGs)hn2;YjHIp=m2!NywlNmrgL4Dc%y5us|K@;Pmo!v%=m&# zExiL$Dch^8-@@VYU2gQUyzQZGIZAH9fS7=mNt2(F$pBF2D4RztYT*f}AvadUITCu+ z;HQxnIPRcHdwfM4#=_(69Cr*9Ax*sCU0id>N;fy56NZ-dZSXCfxA1`ftwOlA7VjRr ziuW8U{@Q^Cj_F*R_~=cJ68gTcW7R0546OwVY$f!%I&e^#D+mNWzw?|Vnv}VkfN1I{ zc~}xn%Un%CG;@^PC5dKbu4W*DJ@XMixXcv{(A<*`(7eorDa0==JZ^v%Wv&(gA)bi< zA!RN~x20z!K+7^$OMq6Ms{vY-xmp3V_Ba7rm$_O4wDE)kv?+770ch*#4A8dB)fS+g zr2MH6n3MRFo9Z1QIz{Z!i@RTC~z46hAJY+$&gI z&?J)E+pDxLwYIjXt$S-*Edp8yTjIWN)v9P!<`NVXSrzhozUG{B?+F3)^Zk6kfBimr zD0gPgtnYbm^Pcz2Wc2?R%zf|?TV?M0^o1SyYl=_1=dW!KS-s0={gXpg4Dy7l*M64& z{{0~Y?+Ct`S@(&Dd*63Q&xq8lFlErl$1LXx~Q#?fQC>8Nf+8&4CujKA<(~Sua zc=_z-{>Sncy_zLv7JoX&M-{k7DvzhX274`9NlAX;lrc@pd2M1{q;zGp;^Ud$nNf(d zO{8K|EYD5Ml#a9OdK=X#TKb>ZM{aUdQMA;!6)R>Qg`uQ;vn0yQdm*4y>>!Xa-np;` zPYeR~?;18JC3Y%SICFI`JH8=ov__nfE2FuqBZ=0_&c(toYkd3%HRY_D6-LoL8mOS{ zS5sJT;LNTPlPF=cren0l%zvSo?{!Z{4(hGHSr=7|D!|&fA`>4vsz9(GRj?cVh^cau z7g`?yRz$Mxj%r6q#O95~=^nnRzPz=EloppQ~!BDXL9OE?@)Ta0r9ZW|#5jfGb6u z^RUYt>>XOMz0!7AhQC;=5J$z#3g8n-u-Mb)b{>ju4aKCmncvu1rP<_FIc>iNavQ)4 zGYQCEEeT@s&r9Z(F?F(`S_>KaZU9Yj%9ZBr;mIN$VtYIEA*Rc^) zRX2G}fk0KdHNsk;qGje4(TY!JR4Xa*d8Blcl=>uWyEb1lsdCoI0#maZF}8g{tTXzikjtF129$ZaK^jaj>+4+yNqnR_hR=@H;%u0hsS zwTbnYU5r*YYij%gOCO(jU9nJZGwbh`wAv?Hx*~pF1d)7t>dM3Lsaxx&2JG)N>Qx2N z$fY8WRa%ujQ=7Qj(g^Hd_Lz=DZ~Nub%PM9AO;+@h^5x!J_<7N;hy9b`l4>!M`)^AG z;g;hQL@;Bhp{;RVw3HHSHSNbyB>i`5+N_phszj0Ifuz^wUc#apAg@i|YI6Yc&jGpV z#R$`Xwn-lUQ%&XpJM8o!9^uXADlv2OU0Hna`Cnp_b!%g_YFXgnu(ASo=%}LDgh<88 z*on@9y7nfE5^ic#uA3T%ZZ)zXTKa+0sC~AYrlU$afv6JNz{puYssP1y7w~!g6RTsT z&I-={U4vwB8dg%Dl$i$Yzu`P_G_$ZqQVC%L$kD z;8f7eP2~;JW<3j=HD(J5mhsi*?sWFEi8ENbp-S)4r%JE^{`ax@2jr42rAxJq?NP_w z+IGENyj1|eURHglcReA|wZ#@}JI^y3o z<;~ILek_qes;8r+FS3S;6;vl=E(VVl3h*o}W1j#lX7zQg{NMe3dwXMZ6_SF%+q||V zErwUnMJ3j6$Yq6|@!_@KllgaWHB)8NkU+lVr-(ZXa=f@l4{Rc5%_P9Oo4${bJEFq7u9;^!R#5xS+c!llBGq-+4uK9xL%(PJPrZe{xX1nco z^JC|D-G}*K0(%>Eozk@xERsMHlMiFDO)- zjANfU7hJulq+7INW8BTom+lr2XnXki&mn($AQk!aX!~uIj5U=_6gNZ3pb=0{Y`L>w zC|TE>O4y z^G@riVg;j#pAI6qOySm1L%gJEbtH}Vk|ugdGpCa@%O=g8qaXg<#N2uOw9dWFM%igh z{Q7rFZ=E~ePidKZpF*Up)-QifpSgeaeww|X7x~eQwgVsyvZY=}a4AwhM8Etwed@Ul zN=2K3?^L&B>yNSXQbKGx*euZc<%`S$wa{L3|Gyepl&;tdE5+jS9$7=dd zNsM$ca+kS@di9)YV&4s_co_1gELQu%lJd=}C^dVKxgY*d1;rh*Hh5XbhqKn1t8Lbc z2O%V42Wh2zu9s~V6-{RqN0sH9&3NVNC<-$A8BB>=j~@ip(Ip%{H>uVyolhhuX4j?F0jXI&#_mjFymn{4jHVtI)YY})V6*19JG2pWXzW}LD|^Mi zt>mGiV$0kDo2sW@B!`-bf^ecn3f_%};NqWAw-h z-=B#fTWybMTHGfvGJ1XaVWuyu(#7gawlhAxsVkgT9RdyM~E zz)!afxT%p7y_6g;We_Ptyp&JrsVr6XD_F?C7wa)0{clnn^#{SGXKW)=>og3s#B#jo z!A<^izWQGJxhbHZRf!gru@H0NJyzV+G;W@CaKNWMc|L&?3EoDp7f00o5~?!|K#{$Z z*T&L|)ZnX0VtYUxnDA?qlmyGrHD-3o7y6_IKTVXcWH-l>*YB5^p=m=i z*}bKlMX{|xmGg+$}4Y9 z9|I8y-gdVazUx&o3z@hkIkUL_`CRiB8QbpmaO33Pz=eMUcmlfHcS%Q%?AZfZ!@z~b zki2z_x3%GRkl8w&Q9ry8_~gV`S#2>d(~dSV@<}nb&_-?QA$!$8Wa%R}zdq3PJf@GE zx^K<~?6J~6w9rG|TR$>0MVOUXVMgDq-*hDPM2Qeno9?CfVmyN@g%<5w*ztdplRBK% z*?ypwr^FKdO56)x&UyOwp)M*;f)!ph>Qy71F*&NOV#PH#nSE4$#j0x(_C&ShVv+gP zt)!&}?}LXYgsur3{W!AlfPV_02-$`b@%3B<#H~@dhaY^FMhi}DF%7p-8f9QeMrn-i z2PwJ%cA*6@$_-zf)Gx%vk6tnoo#|D zkz+_}bv{A$Av{g^WvE|F)vh|*9Gf{ct;kIrO9zg420XrMZ;em&8;{M^xRP2@rUq|( z8qvl9EBu+XUeKil#ng7CyOl zNfuIhDgm)pZn5FdyeAY+EYGa=9JlmMw0vi-xAkIeWkUgOM8~A$6_a4Wkwo5H$RS7L zG@PP_65=`|{cW@)D^NCy*iC0kZDfI3iHM+DM;0mUR;+g#9@+^~e?imfy9fpL2Q4of ziF;6}0ScPu6m)2Paw18Vv#GpWR7?8YY&rQl6=+qq#^eGM+Z(u~)ND0e)b?-y$4tI| z(R8PL>8ov>f)Yzir{N`{bf9gJ-#+V^*~7t|M62)@D)6aeX-QwF~k@U`!8mpM1Xu;dRVC@M^W%>_@&DKY>M^L9WT!Tt5kN4J21ex#k49uF2*a7PQ@ap>R-b&k=MebEnEggEBW| z%M|!!JbWhSQsw+^kn?ZZobO7}VPsBY9;ivq=ah3R;I=SOIUfd(W^%sowZ1|*pAT~W zBb)Q*nVf(0axPQOKL$A;$>yBs=Zs%X%WcYab&%_wY_9LwT+O-h3&~Xro}2MOu2Vd$ z*Btc=a@COQ4COj5$W@ii^(pcgoq1t5dKO{h&S^Ln3|hDk(ZFB7s|_y{n&Bht*>4kD zE1d<;yQyo6D(CF%=FF|Nz#3dUV>i`}xW39|$iNS5LnDf=QF3)0WJL33_!! zQ)F>(tt{w4l`WMA>S$_Z&Z22EVKtRSV=uYzk|~KZ@>)jibBQvkXDyZcXU?yZW%RU5 zChL>B)Kb~cEAK8IJuNnJ+9iCV2^L|KVw2BTu0oq@Ku+Liu_$uMLFCQY7u>~FlO|1`F{3)D_mT^CTW{5(X>mJ%vo5%J@|0?C1hHL4 zPz?YUjhcMPsXxBpqIlK9%O(pTZV}nB%!MJc_>@!hX~-1D(lV;cDf+01XsPUrO(UdH z+xbAgHuG`M@>Q$XiL7O;DXcLC_UTF9_&#sra~r5(9?y5dnXHsTzBgvMDbf315WG z!2I^T_cV$5@s>drna|X)l?*7R!KGR$dl8iRlX#wYu~tJIm6R&XFGFcRCe4&3`R8pK zLqB=+hOAO_=A%KlHT@&PyTaE^?r#~LtX18d$F94bPBRNuzFNg?;p&_Wp}JxNXOeZILm4ZbT9ZR^V}pp6lD4~nTq^O6FO%4 zVHn;l&Qo>miLojk$9Y~G66<-?N0m>?lq=NNKW4u6Z@VqnZ|hM0o=gr~{`ijNoAi~n zg4mg?ROSl+4!>=`Gb#=)*6NBQ(#d(KW9DB={rnkbNbsiDo+Ra zX38InwA)SjtagbP6jZPgiz7COtHb9_s!IlyH=9f7fEEQUOqZvWd~Ee4be<@>p!3*Y zHMdh5H@i;tCN_EAr86>v>_a*1yd_OtRLXM#w%))C=ab)@^b1Q-O*g9ANo;9->dckC zC@Q;~--s?u@f0CkhNT(Bm1;F3`$ft^pprlsbT*^*JB{~}7cwgg>62o14K(IgUDZMb zw!TO}bZv`#y~g&}>t-DiktHJAO$*M@c9YZib83ht2e*&Pdhor%$r&llcSAsd0dC6@ zFYQLs?3!|mDPhRL=QsH*T?fygid zV0{YhO4*!CR&!g3_vGN5)S2ImLP&b3mY64QGbPa^tDi4_y-fxmi)VJiV)>Je)2$h2 z%4-vzb(#xTzbONGHgLEkJ~fevke2K1AIRiC(PA+wy3lb%Yu?=&cr-EDH^^Me6m-PB zIIc!3%PhMw^#IrRC#!549!DiWXn6K_pX3lcGucTE+1+(Sh`rv#XKv{-x8j)@-*t1} z6!B;$q@Q;dcC-~BS$uX=V{+}Zx)o2){ML-;-Q4Bu5XV~F#JGaCX7;D)`~Gg})7CXN ziX!y5FEt>WZf$=s|7H-o!aHnafUr7t&{=T3*1c@LX7+1jLdESufJ!}pHPUy6K z7s6P7-xx^l73pUm3c{<>FA~nm=ZrsyoIyHGFcji6X(g&zv0LFAt1x*7aQdevOfwZb z%(^?iYVL2+M14@cnOQ2&QLLxy!Q40v%QS5Pn5z@p zd|QPeJ&*~DPFn3ldW+cywjmhQG=6x+aNEz;8as=C5oU_;%r_qhOMp=pCF7j36FO$L z5HSjc<|$I~>ddkky&}0^MJrav@^$vVz&G;11+}#Ws?b0!NL*9YcDI$^*2#N~Tl%{B zE|k>vf`?#JH1O2$YZfpkJsLozI56^TPAk$scrXaBN}o%(lm4ptiKb>ww03GY^>8zQ zvC8VT>z*VSqR564W*u8&GYHU=Kidb>7I#k8uKTaeF;ksedD~r$YQr* zWTn&aDC&7&QvA$@ZHLs84~RySvx}ukOov3d7Qnp5WJQvr`ftP?ZB#MfK8O45xEfWx z0_5kxE2~SUlK#h%c_ituR$U}HZV36W9#s}ej2hx5e_;)T5r1b%G`5xkQ7TG$L&6&f zpZRFXRHyWORnPUWPU$cF3cV~c2RpjOrp~`)Zl^BWn_X6w995}4;})=xZV4)Mw=7ce zhSP8XkX5NC*A}Y3#22kW(?0fbc_^mCa76^n7ui<;;!Q z4{$UzbB2apkhO3jP3!G^5JUU9XdyXNN%9Y2*y@d&yx9T^U~0`0XJ#0@t^h=SRI0g3 zH7Dl`Y9-w%#im5lmPUr?!S*T@?l}#HhdP^7DFjGv@5*7AtJ20TchS!Lus{3$c820T}|6lNr?tlg(nqih-++M3^XwU zcf92VqJ{1Hr`gTlO7-@DxcJlO7<;t#qBmj#;{1o&qZ_a8s z-(v&12xy3L?oPuQFHB=WpV4r*-X+j060ci9`aKJ7tKM+khZUX~QEH_qo563GDNJ-U z@wpYDWgSH501(uQ(5m_!K;`J8hUVr7-*c*+Y7>&Dhe)3O0muaMG~UY-MQ>99dRIuW zrbB}DxofCCqrz-4e->#VSR3EI;;=oaMq4ydjQ~IWiJd64kgT3l_RaL1M=0s(BW^~o z`M{Em4C2yjmYNa31Q|N?VfbOomD%}?{V@oyO3xx3(q0*-onJhUUbBmW-KaMhhn4|8 zxa;|{FL+G)2Y516C7w0Er%g25)8GWviT6>soX2jxjofBmi|1#tDUd8-MfyQ(9FAd; z1ee&OXL7!Zo9)+|i7+WK->Cq#t+zM6NGnW&KaDD+i5x3U#N$q2Otp7D@$I%{^D_Ba zr#vIn__iwJePI9|*h~CQ_tV10(SoK&dh*ZonR+@{!vc7qPh@raYuAmJ079M@L{pF2 zy)@BDB?ts14Dfxkeplj>3s0YKUm=X>^#x}_LZ&Oa* z9}OkNN=fFwKLl$PDnx&x9tg$*zFCN$k;b0A-t}Je+3b?8H$61($zB_2Pv_Q@n&&v!I~1uGPCR|B})1wf2Nu(FOJd~Tp}ieY?~WO zCQ?s+?08oAucGlvq+UsiU03hMMv&HSiBI2Re)FiL{AKF`t=<)x^K3UH$x~6dx^6|% zx;mFTD$aKr9}hYZm^-)#9@KFfo~GwP8XAw4 zAaJ8P;xC#`mJ|YSrJYdJmcWp5YL!a8E9^JqZn``5}fg!KUsyoElPt;rxbd znc*DuwZlo%4R1K}31x>fBIXU}ty#V$$-{V!wODv2_Fq7W9QzOfw=FR`lY-BrZ?I$c zub`c7_FYnl3vC2IDgg!w+8Fj4-I4A7?D^xtPC#r>i1Uqaz=q3};K;nwu+gadwS z-6VaZO@aCORcilukNN7vCCl>M&&}P!j{22ES98Y`Og*G;j1}iZAh~R2+l3qsb2>WN zLG!>PKTtRL?Jr<(`Vz-%vD{I`{9nfZmHaQ3X!_2ez1DgQG_tA%{FX$usBj`^`h{9I zk^iTg{ea5SViGB#Z~8NzG4X6FJaC*Chc*?Y(Xq2)o3@Aizydb`N$uNW^VBJLC_!_% zEsCtp?mXBXfyGQldrSRt)sInaN}|4*(3HeYEBP^hwU7f(GW;OU3B15Xd4;Kiu)Mij zl|JH*Yff8&sccPF(DpjOfgfZ7chYU$I=Z!!OuEtMi?-WSJ8aJK(b z*A9j>1|es@{9DUqyqK)*Ade^3;-%hI~+6d97^d* zl!nKs%G(>L@^>!0H0WUIo3vu4cuj*7V~RR8erK82xKoO2jgDyP6h(3*7Z)|h`l259 zry1zt0V>*1Ux_EWCUT7#akct~X-wT2M0Y-;9@`x?xodZnCVXc^1(^}O!-!VkN3I$M zQp3Yu4Y4yjmX4hnTAy^#bDYKs`X&`QScwJtu{xSQ6KwJ2X_dFJQswW=EcCpAy`32# zxT-^!?e@$qnJoR=5}EZ%$9kN`qi7Y{%p znp-Xrat;4I4l!$i#usI~t*jvcYwx5lEYI~a>jwnl&ywlOmZ*3Ru6ZgePjgT|kW)VJE|E_=i-;wk`^ z)9`00P!(9iV-;5K2jPNxuj){*&Uj%~I!6`1z-S`%t6Wy6LLYd%ehM97r#Nz4R`2;chD&Zr5mNV`6Wlw z8XHVIvi|w=*xBgkG+d=>C)#RT0r#+y`16AH@$ZT*a`@sY5{qcE!MiOz95cR>=S2UNiGVtY@O3rAVc@|(TG7c=|>^^398R(uqHIo-NL9G`XSUg1Zv5?id5Vqe8yNpH>Y zV~f6;Ca3W`;--Z0+mCuls(|D=A%;z7?PuO3;Y5YCy+3jB6=YV_WJn6w? zc*5h#aE|`zi;3FtWc>u*G=qSRnQ?)&f_-(7Cl&01;Kg)`Eo)z3U;{}{C%PN^h;I)- zGI(Mknml$_$$-e`9tnuviU`S*+SFAOwHdeGGU%@t5tmJsh!{MZ0~GQsMR(#nCsZ%V zptl+$$+3fKlaq@h$y%NL86uYdfmJ;s$%`vFFC)H>bdX@sH~g?`#%G0tlAf~ygGIzx z7|unjd^y2~S3G&Z=0HsyU!Eq*xmKpUX+?(8R#<+|hqxrmmD&nGu+avN2V;w@6cLx9 zd%BpG^%%)^Wj%;8SJi1qn918sPbR>V1TerlxhcY}*p11E16KlLs$_zd0JWU-J3C81 z9pyvO*U1NYmYIT!E#h1GP~^!6YMuFL6uQ7ruoha4R-DhC=#VMYCwY#*apZK`K{8nN z-D&u)s{99|h}7eQJCt(qr`?J+4n$gi+2qJTF3b*<_O8Ql8g|f@nI#a^uW;Rpcj7N( z!~*=viiL9!jj5^J!}HGN{kf0Eo=zw*)hbi*&P*&d5(k5QD27?jlM+jvSdTBSOjp0h zVynro8)f`OJ2+U-Yv~DSSy}i8^j+jMUJI>>zJaAP_k1Gh2QBr@p&#%8fFqzfjrZ%* zTlN$D*N(f5{WTJTu{(`9G?G4&e^}1`07^A6x*KS6h3p##{MPDg!F}e|AFb1sjhGZp zJX$7&cp>)CW9|UrAOBqtABxt0oBEdc$X+srSL%*CnR~X>QZtIp`{#=Q58KX}IC(cg zfLqKz(c46TDrPC0`KYoi2tyUhUYW*<$G<#iubrY1`RLQ}4H~+1_fhIn>=aeaqIZkw z!Z7W=G_oq+oc~{5VK*a7;>WNK$X704l@H;pe32Zn}r#BJqynp(&@cfuljXpdlsP$X_wktg4j0rp!%+JE}my;|!A2>7w z&x;|&Zwk-y$$Nw67~7=~JpcaFFg*KN=vLcn(epf4kUhckZ%nRP`PVQ!UuP=KXH*fu zZ7r?}LAW&(evfcxcyj*B-W_2*#m09K6|VM6@^3}G2;iyKPo;IY{wgg?9|4e&#D~B# z&b1$ldlJ>^wE8E!X(Avi(hW?V^W{EEG*3^uEnt3BiHno^6qyd~>D?Rct1bB1Z^L;Z_>TIq;M;A9ob1B*dkqHU4E~OQ{Jt@K^Qm#q z@IBmiHw547ExHHztH+L1_-o+%8z{;A?x8Sz|H8DIr$FKWzO$2A_>T3%g70*~S@@n^ zm3ST`)`9Y>#H*gcp?=-&1lh%$5cC0ni@R$x4>Dh=Q48+_>m81 zMcxDGTu87)OhoY)Y`l1heA*RXN}SVB1qbKmR5z7L2+DG01>Z`LM$m_Pn4!%hRa?Q=-O@q;{0n`~69T@TF3Ji%f4z|!e!$HD zV8Dao)1Sl_v^@d;IEHCA5vI?B|BCd9_;Cc`zM=4ugv0O`hyG_RWAn|+onXIk93MHI zkn7YK2JsO}fjM{mWA`3u(T`_>FRT!uZMHF2KeaxVK}!i!{&Wz2w+=~ z$c8@(`D*PT+*uw5`bGV^=^cdORGq;*1)=wEiX}Dy?(?0wtyJv+^d3Ybd(Ku@lh~_7 z70t-NuIE~qM!($wrkpt3VhSKgP8@Qmn{C=%@V1WWqEZyLkIjqBk>FV;eZQl^mO<8Y zEN%zLy2OGMH9f1Par52E>A_Neg?R#u1Tk-6ssf(yPzXL(d<}d$;h_f~Iq!Nf`7Hz) zhRIPs1SWx!upR(`XG0Fm&1uQuO<|9oElj?E%z(*^ufwq?sA3N|mVt?D@i2hN13vYK z5Q{4hLj(*su4l50@G-!jcQP{h?u4Nvsof#kOENPL;t6sYpoB8Y?~2mT-g zk#_(O5E*ztXDs;ubp9p~d4MXu4k9NqzAUNT=F{okAQA)j%#DC9fJpZcL`p1{*!847 zXYA5@gvT!|JU$8#1EFvw(jr*c6pR-T4*O1jPZ78H@F95m^`rmPJOniNIC_62GsMzS zfDgA@Sagu4o$%puu+022LnABFk+}gRR;AA;{NKcf&f{-HSY*aO&Kv*!;qilv-)Q{6 z?rXQ}YnzRiRlBa=lGsbVH zVGKJ1k;G=MZ$k){nZq=U)Zkz7HW~VYWtrTbqML%WGhjX*gS6+qsU-zgIvezMl1ndAa9-A@kG&51UiRX6L1vreraersQBXpz*YVzoXf z#`Y5Tok0y!gjQ>#CiReYh{aRWp0n{5tX!WPcD6Qss4na-=}B;V_YWG8IZm>g`dJuP zb42@XJ*_c4nw-d+t`hmORboJK8up>VtbI$Fi?|F+k6us`TU+RW%PBr2x#+a(r{ ze!6qrx#4>Zh*REq=(0G6ir&X3Lf_onr`^&g-CVXpo^={@xvR6FM-$$x?3y;Lu+h_Un?`f9 z!&dGH{=1jZrD-&G1aIYz;5)s9u1%x4*lcT_(=g9V$ZHy%M?${SaE+Ic-!z(gg16dx zf=}D0X*7M=YVQd?ty|M*?g`#%?+HGwplLMTv0Lpu!KZa^8qMLIt@fVa(|Rl6MR~)rqO6iTkSo;BYHQT+M9hL3rS8xNN=0? zea#sD?VHUe%XTwQnx6_60vR%RAmf|o70n31=ub@^7`$g>&_tr;$?WBDD@UM!PoZwk_;jce!?mIC>`=Hm6pj-1&r7A>_)_7lzTjJ*UeP(l z5QBi$bc^MO(vsA^v)~ofU?6TTKT{)xgFzZ#VJoA3#SWe#SyeZ5TI)4n;q`n~defX; z`SqUo)d?lI&6k9F)PZHarOiyPO|=XwCR|7jUW;Uhjqk#+3e4a$)Zqyp!y-fC9kHwR zb?xmeuq?YuyhE~>)mwg|n3bPcU{?8wp61Mb9>kma+l4X3%soU~?3F2Py)E%}U{&{l zoMKhle0IaE3f>d4tz{9^OrgHc*cmL|%v^~~F=98FSF$@!WtmJvJ6*BN%;!&Jzfv#% ztkea5I)L{FOEq~Vo!Apj(0gW2_iJ;BiTByl?P96rJ@D*5U##wS?9$`@h z85Wfbi(<^0(daRR{pSItJ*VMht*4n3I5C zz*qlzWi~u76n;1qUK$E74~0((h2>G|^IN?FwOf5`@BFPb%-?PXm*8(bns(3M#-1i{ z@5`#tw6@ zBqx~pKjm*rt&sok^0(h~#<)`S?VC&e`cAQ{;`}){4QW-mq85Ql$m|(s&|>;Y!UJ8 zjhSuQC&bKtV39k+%oh8FJ2JBgotc^M4-gurFy5f4ZFi1Ws_ns={I94)9sYR7dW!AzPd`*PE7U-*NF2UCegRfMi zZ`znKXOjryu1=(v=)eu1oCTklN-uSZ<0Y9g;;gngNoC)ba~1&f9qP~i-iIZ9{EXdf zyckK|f62EvL^!)nvvTmPi`3+WpPKxmeA=sYQ?edBYDaL7{8O1nj zum0cld!1dsuQ&6nBtQ7?Q#06#gABvH9!i9CGnlZee!SCEkK7wN1oDG*=}@Y&i=d9{ z_pjipVCS%0CMj6I&$lR+Vcfa4}o@NYxm^F!f_2nYN( zyFa#v{hto|9b#vwj{m`3L@W6tDnq+spJ)n_N1q7eguXlM0DaDse7(iGcvX5a;jsPU{|FxMUg^Q(z*9o- zcn9EU|EHbJ++E=D0kZ3xz~cd`_&Ru;Xt6hd$88omdxuAC1n{^KzyI5M7VYK@FG0emNYa3O}CH zoyXozjck8h*o4Ha?B(yX>lM=V=1%|-QRJp(=$wPO)kZm?IiriBbBW@}*A+<sfcF9>(sZwcdDC-*feh2G^e+L2CdVl5nC+Lc#$B>3y0 z>Jk_DbCJL9-9N1Ap8`{)^qqP3k!!U01xK1HFcB=ug)k$$SP2VMOoAWKcN2n5); zF98YxY(KNx)|nNE#7Efo@?(>)x^~aBGI;Y#i(3KMHu_{80-MmrcUr6Zk~VkgPKz%eCN_n3Xn#1%0;_&ix%uh$LM-bDi#}=c z2YH6xE_pU)riVN~tN<8T9;|RXh>^g*r)=vCjPHWZ-vk(I0q!19BjeFz1+1#9 z!JOw$5^qvwnJKbg@5+1??Pk9g*sl$luQD<5Rjo}UF=J(FHxr3!_fSh9eXg_d;$R76 zDv4_ql3D){cNTC(G>Y7eEfngs%s2*ulsvh+gJo)O%ZznEV=v2$UJ}*cV40C`ml@Tb z6x-we;2~Br?QWg1*ZskTR32Jq$R3|rXZ)}Iot;*HeNj0W?Dy6euR(qxgU&$M#NO8z zAD*~3JP6hoPD3H<2lIx{GIrS`%-P9xF`iMP-5TMYng(m4s7-R`^g-8Ni?r|(uwhNQ zE^NF)QSV@h|A$Bc3zQ5T7~3dL^f%y>^QLCQ z(?j7KL*YM!!gqzj`-j4Z5oZ5b)h0G%?{`!@bllF~Z@g=q$EC1DkLvGs<=W&#>*nRT zF)Rbx)lie=Py)$`mv7jCF7aJ^ml=)cfa+LH-UcoTbVcq;^Uvg zI_z2WZs`k7AY;~kz`=}U{#d^2$bg^6)Q|^Fdr2`hox5xMRcHBLt31axcqAob-{Ce| zhA?>ZXEeh@(|u~nKb&*Q5wTuo7?gpO%!{VH_niIf7CN-wp}vh&k*V*VPW7#0D!;Bi z#&ZV+L*wD$!0~RXW~#TfJPVxQL;}l+no2KaikHGYFg4tVr|svnl)_cO=?Mtq?3CE~ zr2gT1tG|Dz`uFKn{}-Yt)o<5Bnd9a`{qJza$sYT^&{AW#|Mz$)q5j_-)c;#li_ZPO zYH#(A?^OS(o$5cqtKSTh<^rOiOoAxCCpFtxfWN@2*WTJQ@YRm<@w%518qZ3lppZW+ z#+zyHm_4<3$NIg%Z&s)FrgUoWEWf>TI<&Wh)ZL7ar#sU(kY@z2aXiu*R=E5yd~RHCd0Hzrr^ML*rEi%7vf1<11vBrL z0h6PF_$E0GoN@Wa=d1q-dYBwrZTNikvFx|{{n_m~_Si)8%<)Wky?3_yWgmR!amE~A zT!?|mJHLK#W_+3R)i+VjKVQxJO2Oq_T34lK@hRYc+4IHiLPE_>{LYT~xdNk-^Mt|q z=&#TuspS|K(ESmf;eJoN$Q)#FrI zaGpAkf(+5N=cO%8Z=h#=TWD_8$= zrK~eow--h#zH$>RxYzU0@vCm`8_utmvqKaqZFLsB%nfdF`CZJqFn$rwOV*V0sB1ZW z;|{Mcz+2>|rC>jn014SWAZG%*7@ z)s-}z9|8qf41bF~*lbLhP}LrB=7ramwmBMq=yBI3%gwmyNER=ilU0lHvvo)4)s)nO zu$90oc-QrZn+tMsFc^NB8hYLa5XrYxiHkD54Yl7=Q1<{F+( z3cXX!lwmk}i#MW=;%d47qQ+X-ALrW(qNUrJ-D^(c>VAnl2fm+d*4dOUO?5bb@~~9Hqkc-)raG{B2i~7|`6+o#b+~`- z!2MJHpsFXosSf|o9s4*9SNkdZG}Ym<$epdz^}SnD9bTY23SvJ|te~lmso&8(c9vq@ zo9b{2+|eU;s$xBw>e%ev(K9wgv7SwJc!KUIj2)p^VN)HhpgVfS1}N66sSaPz9lc|{ z73)!t5-ItlFdf_WiOnXq4>6`E_~C z$RncvvT*6$L{{DuYk1m5N9Q*2)^mM3&|c&vbZK${Ht&x$+~*~9ZE}HiJFw==6p$UA z*W?1~c0hf-myqA&0_paBVps5WpC%VTw|9$OM5J4j3!K|6oJSWlxoW$6>`cCNZ*qZh zdyiNRkseJhKyC-$iS%r8fpL3b>_{SoO)emA?-e_kNUtUr2)Fl+^&!%`$?cuhIW3%# zRJ#23hMxdK$o>ET!X440zzGg!xZgF6lm`(j2*3LAyMGH z6L>4yizpD^**oSq3nGGeM&HbRBcctoT^LFE%f4QSzjogy%LRBu`8E6sb-zvdQyKl1 z=awO+6DYqd7wn9$Pk#^M;84GR9}wKEFsG?dvp@we#@Dx{kT&;>L)w%d7RckpH^V zw!3++(JdY_ebFt|J>Z|fcJr2bm8Gt=6?^yo*$ZZHlgB}|&&bL^lL+3~vbr$pisL3` zv*Kx;+1*{Xrfa$vN#>{D4QxoQ=5fNc6`L;0H=iBB;G{^@bA497!x8jrtoDQ5e4w-z zRGpQ?=4*xSKY$egB}k!hLIzU`H#$6Tqh3xLM` zJE%CxN_!9dTOQ_y?t!n#ATD=3V!=v|*y>#qpVkW0=Oa*Tm(x>uB^>E-=4u?gPQ8!; z=eSPIGL9nUcFDea8L4P-=6=Gr(^A8CiD;VEr52K-OS z*J0aW-SY--_?j#En!ZFtdzZ$P8vfgJ8Q;KS67D?M=ZC(n<=S<(G4>GAc+Xkr!W4Vs zA~$4|%5dFea~+@O?7= zQ~RddT2(|@xN22@{Q?>0wU2g~>ufr{hvTa;?9U;lOR%_cG3s4^HQgKy66zXk-M1;m za`8c$jH4I)8{$q)nyA~(iv*-NRIZJy(H7@Nj(3mb3F-_v3F)239$BeMbS5v=Pxa-v z2=L^XO83YyuCywVm~NLk5>DVe{6?0ro_2Fzd`ZuB5 zw+*V`i2fmHR18&nqpH=d_aS`ZbWf$5nhjJwHSeiJ(BIDwA>zLa*kZxx_1$SW1N715 z;GzDfF>AM*Gw33#>OqwBIk)Xk+o4`;McWa6a`tTYR}91N^m=pNfm95eKZ%iK`te`t zN8+8fckMb2p8k=4pYgTpUbL`ioJiZzL~1HO;N+oBX(Rq4?M`X!9H(^bJg4;B`A+HN zzdEHCH#?>A7oF0pR)>(fk-Y=-1(zd!6)U*}^xAbfpr2d+i;}I{2PHAy#!|D`t;d|0 zvq-3Ey+Q45*3ne{Gv|;gb&g&QsVQo0WH{}`Dp&uX4Q`?lYGZgQrMjucO8q3>BOSX# zdut;U*j`3vbLt_8O72|e?lvxnoU}p4sNv3nu1mzBs+#0VDX-g>ymGMG_Bi8Q<22r- z=2DY4cv}hnJD(g9oN?%VdGViuY=zE(zEw@!0eN4S_^@%t8QAu!nJ0rTe=M?QTj~%>aEmw*-C!PUrxMKzZ|BY*E5>Pb1If!w!ZBl z)(^?PRbDm8>b_qks{4G^c57m}^H_D?ol}VIY-=*d0R+!JS2d2;A#a+%?Hqz;*tUD^ zI<2LvG0L%dk{}$KsD_d!`KFoDxmUAshd|nERvl^FVNX(($In6NGP6mF1q;+-H_7=@ zE`F|f$!W;ZE;UwR5?yN^MrgYQEtN$!b&wwAqy-|}Va&|v?-cjQ>XJdI_(;DiD83lM z46xawm*-R+ZW7P`koo(#zcBE8iTG@DcS?xU9Eo0Yb(!XrUEk3Minv~uTWkIH zlVgUMn{AC3aXW0NMynQ3f<&*z>6scyotmj&*8xoR2B3ZI%m60R_ugmF_aoF&>d}&U zj4YaXv}BG(7;1F8SY+ZCe!ANAo*c?FoqEE4i}L$dmF2rcf;TFjea?)kzge=s;N(5M z6K>;Ezm54SXU7)23KYzb_c)TR^X3wpEmQC8@zILyT=u9sQm0RqNV?C~^QhOKiWi&) zslZlo&_7&BO18upuJ5%MULzLpR~L4Y3$4HsO z$0(b8`Q})g0%PQjLZZcfv@%+;hG-u@%8r`8cCk^cD=8hsv72n`zpWaPM#4`)T8e z^ITQ>M^Kc$Pb7YR$@KJmKR%VXcm6wFAlYI0u-`PI zo=#0{$=g2zxwOQaZ{L|tz1|e1`(^5N7I?2yHDTBbo?n_hzU?tIm8YU$>y==TSp%Fx z`@e7yyxO_H=`~4)FAx#WjUoSMESXIqT*pn_2H0|{6PnT-Q_+vT{=|c>AiDUGn_nMj zdLGk98uAv@=541S*AL{u!Ng56S;=4U9Hd|#;l-y=8t+GA_QQ#%ek4g&x}yOOZ&X|J z523BZr;FS5!E4h^*_W2hKWq`X>Sv}8wWwMT@~Udp6JJO@!&T3C>S>vSMXW>S2F0yC z)IJEwP68&d;e{%2xEfBeEECU8ijZmF4%SWdb(3?f@hwqre@efEg_gd1?TRSvE^F(W z_}51FPZQ{jSN}pBwsS*!aNc^VCebP&Z?58Hn&^jz*Ts9!fEn*2#Yh)csQsXv*2 z6#de>(B2+9#*<=igD!zFJBickGDVA`x$noiqYpU^x~Eqhv^Y}#mE+9KCrXIh$e+<* zP@9UJ1vyPDVPwNcf9^k`C9jxK)>Lu_Yk{py%PK9P^_mZr_mAd&;8(7x3RP~X(yLr{ zC)L>BR$gh_q4FWsiS5;iEw%}+&7VpVG}*H{aaCO;8Amg%nMlE$mYS(K=1Z_i;e&vu5r3UcT?F5Z2`mTYb+0_D*!2_G_y|}?x(=&r~IiM7?7g)sWw3P=7zgWWY zpa`XbsNlf~8a7)@U$Is$ZY8Cr$iLo`dV}(1l+W?XmxanRWcDsq{uMAZC|{PAx(Id+ zJQeKv&AyOa3@JgvzDj5!K^{6qZLBJ>-PqpxDqcG5Q~o!cY=iWF5cd7+5$_p%v!7F= z%6Rw3!a?!msJaf{t^Hp&w0X}W+1)+6svpuB@lM71{GnX(q~=ExVz1Nt^0pr`o&`jK zrp;^>lUfLb^=BnKc)Imlm&X_P)x>ZM>6yZpH5gMgP)s>`SZ$5TAzH)oPu@WvEa?KF zND+D3LSq=ODoH3D1sFm9J6;e51aBRLu_ONNT`&Wvt{+9O?qVa=^kNo0h^+1IfkuBp1Nig?KE=Eralo!% zK?&U23$W;n6kyr@eGe>Koep4069tP3GoTSv9<^klX|Q0~*8(Yj$}V7G0{wNP1&bRZ z7{KzTkSz(&&1Q-RV7ae48-9fF?(c{GzX_iK9(CSOk1t2igIA7T|{)gqIQC4SZnb8~jaa ztAdYZdok53m+t%f)996hdpy@c{pON2z0+wQ+FEbopYzniLLrb#jc zq;*FF4P4r{4mSS}v~StSt>W!#u!H|5<@I9hkGe<(&lokQH*PUb(wg?$yxaCYO&O`< zsghL*vn01`PQG1-v1g2Dr?kq%2Z5q$wppqdK})Bb09k6Hianh!>PIW3w$*uR+Yp9n*XDxI)Y{I z{Nc7Lz;b#aU|9fE1F-0PS`Z#gIBN?Ee_DxB z<)7jDU-wc%^{-Tl>VF1g>|Fn!pWI9Tn>y8hd8hi%59+^g zxAoWVt^Prs>ff(Z{dqzCJ$GCGC#`$w|C3;PNB_VDUP=f)e^d(8^^lM<;~&_YR1QW)Q}5}cXw9kZwQ z?m*?AAU zD}Uxb+sO!iY?pm-RR0;ltpBw2a!C!ZA<@i77T_YqE&BOmi6c}B$cU4Aa`?WC&rR)n z4>B7??IgZBrTJyvPlfk0!uvU$9~E-?aGPif_=SN0qV+PFExVaqoc_gY?*H1S+B$n;j!Jgk{Rbgx^C`K`}M0h4f$#O zqbM7eytTs?Aos6QH;u1?et>26k$O+Uc+vqKJg^;p}pRuAywPNNLy>F@K8Z(s3xkN=+1FAb%4aJa$R zzPMYsesT7yvC-5ez*7EF)=XeM$&UWp3TD9D09kKhP>R60nqj-idKpNB72X4#$@xSh zm=p`NS22jKvRO0v`qbyHbHr0xt~Uu<%CD5>&z&DVImH>oVvv2iBggdr-0p*-Q?Xq# zvcNsED_bM^m{#)HNXaE>2wNs)HZAu>Z)4Egb69V#W-J~AS9=g#;U=E0e?M1Ev6Jq0 ze_9PLr?DqLcg8=dt|a2Xh9!Uf>TIs%HWxXX(-Zj$kZ##0EdO=ChVLMHOf)%~Gm+1E z@6SbaBJh<+;@P%-zJ1A?pS07U5o>8J1c)S>Bc)rTIO46YWn+&G-U1JJ!%8$BjP+*$ zy4IQ^%0OW?@@&*y+pVS-$e;~nyCEn5v5!>o%*?78y>Q}lE8dFbM-t~yRxXF@&5f`Z z&!`Pn`2flEYb86_nKX3WcUkVarO%rmO6l^-pAnRATN2nGEPk9FOpgRLuF#Yzk&`9r^V$fjNjq4+GWmHW z=EEGj88b*l=Tk*)=4{{X`R(l!*sJh86@C8#y?_Excp(teQ0Ys!^Udrx;~6)1ElwLT z+#3N|+Y{_F*pbFhnya-kp0=3-luL(`?#7=!p zL9utIibvN>-;4CnHb8~(huaFpr!Z{H0UVeYxMERvh1)Xe!GJ5HpStV;4?VRYHd}-& zrsx?pGd|4*e>V9iTfX>UcPkVyO!4F?Y9wU0-Yso0_3b-B1Lg;>koD#;5f(-0XNW(g zdjqyU_{&JF#mPm^$$g=nJAA{3-CX=l$tvWqQHp@BqIud>CevV-9AnML|(s zuHiiKg}B;t>AX6W#}{5jD<+W}^VY?_8{oAZ5I7Mq&3r)Lg0ejMeIf5f`emQw3uQ6O z9I2uQhxmW&aPxp)j!$vQ9jRj9pWs0{o#JDN@2-otwt0Tr9-Xgq8fU{W+GLFK)?=A; zwnp3293k@ICCXziJ^6)UYE!^&Q$H%UGCMhrh3G3W8piMDO&9mmWjpd=SXW1a^`CZ+ zad2M$6n-Qg@g!%jcKY;m6C*cZ#9u8iZhV>LA-oH{+D(mE zEuI6XR_;G5x1D0+^M!jSaWRSa2C z%Zr^pr>$n0zrjtMwt?TR{Ni1|D*iTpQ4A-NIwv1Y+z~s`I#Ipl;Q_l%G>@C9S4U9R z>2v04m5)|zrF<#gR4epk7R+Lcal5EH48ikNXGiHja^JA5-?9g0V2}rXdv4w0Cmz&? zWlWHd7egI5%BR0%?Qi>GQ_{^cyH;#*om26N*yt|%Pk#E(N=gmCvJ7&%X6Me3Dq+j{ zz8aG7AOP>cvk$Zbn@qboj=Fhbh$SfL+yas7A3|PlrotL<%4Q;C`vFMH)LTs#@>jLy z$yeMeYT8Z?7+R%G!A(5FxC1^WWTSieLxR1E^bs(mEYdOEeeiyqafse|_cn^E*)Hl5 zI~8MO{r0?AZ)$?yls9|7&Vp(uj~kMluFF;zjusXxWycpE>a`9aKRn?qr8{&VcNu8EkUGdE2x1OpDRg2g}rtPhWjho{|HE6OulQnsg|25>DV3iPy z3{^k!<|#7i6;ljq`mL#HK$Bh_;B&<*&h=6%%bVL=%@;5M&T_q$uL^K}s_Jp|V$SyY zu?cgwH^&Qbv=ORK5qr5oc`}f$u6X70GdvI%I@O7%s-+WdsQ=(A0r;LO01SM8&jb`Y zRgVk4R2qNKn_p_d^V6R)Dw_G2mu~Ge0gZ$fE*^MI7rqMG%J~#w=dqds;x&LI)2{Ovnj>%N=2#8i!ik&LHsC3| zL?@A}##%3vMKTFFVKQ67Yh3Ev3+P2cY13jbtsFrR+tkAK8%v@-j7i z+wrif60z*WineC+y$(zZ|C%>#tvr9mg>I4*Wn%K&SAd6Pl#t6h_#|0I>wLeivr48r zrN1kgLouiHkJe8?8`AS_+}l8QN}nicCLRI{+Z%A0R71&X(lPHwIZ z$WwH@);zl{ga(@jkIgYUrzY;+KRen2f9HLLxp)U0I6K>%|H+ z0?Xjjb{Zh9>WOwx-kh%)PJFs3N2#eNO6m}#?r?pII;XDI2H&@6m8!0xp%E<&HbaiR z{zp=;Y19sdmTxB5U%|GCKAkM;PD~OdHT=7@wDBJ2 z*UbYkZFuKhLSK7z{Q5_VNmoq`eiu=K_^MZTeJI?^$0fsU#p}(1;HE^^OPD+?=hTuQ z#o>e0@C!QBX}!Q)ysFp(JgWq!%NL?dIyVChzKSNyueR@O=U#Tp+Nz!E4LUYfNgrM? z<7rxBWT*@!uOvMw%Lnvb^QVD(-vLk>qr0ycRv(HAUfsZ^YCNy}leYe7El1UF8!*%H z)^V?_zJE|oPM`QGcH`9iMbsx^{gVxNNRq)j#Diw@+Lcp59dwp}*l_$<;?lRJ>VUc3 z&b-hi_5gbr3{7|@WgkWRJ(FZ*;lg$B#R3p)WE6;3D>jfo5A*mI`O~-RfV<6D1nM6( zB~jbIek*5M{|N#)bH^bF?G^UV+dzK26kvY~oH4^SOALS(ip_MYYOSdx5527!WaXKC zv=>`22;Xj&a!Y~jH0sEK;!E!`mz+hjsJ<4ut4(YZ5~lKNP7*qN>b5RZw=5SdZ#HL; zeMwGE0iU)IEoS6ZiB{;O>8JElvs_u;yeNkbxQREk3st>12Ta}QK1f}ylCns>b~DHl z*fO=+^eiwW4Zc(9>6$}cAhV4>!B-U_P{2uVh864~&9K1y$}R|75ksiq+5t{pRcy2N zPu85Ed?$GM{>#g!Jm}|FVf|6*vx3s}UY`cpqW&OM`nY6GWfb2`f|WXGS!7|A*k(2L zE#~~q)|}8#A}r%U+5YM6xF1cKSHE?D)A$DB5sYd+8jNZaiwiL7;7t~zpaP3g>Ay&( zTKIHgQ0WJ3eU_!0Q#lOG?PWK9wv)NsOw|JitA^zu&FpRJ0BebKK9f2Rqv= zReh)`QFB5%AwXo-uW-eqD0MS}D+AWnjkc4}j0e_nlnB<}so@pMe||XsugNcpP9H-? zlm65K_c&ne0ryb^z6accw|d0$I(<(c@Fkgt5ef-RxLj} zIta}{Brs_wZrmwjRMXYK)q1onmSjt`A|Dd?#hSB$i6=a-6m2)YZO`R9O_!6urFuXy z-P@S#%Mv;-bzS?;ojbR@{GAsMo5No$o#5K_CrXMzv1MzE?w>OU`l}{sOc&Rwek-vo zXPGBb>)-C`mU1ljE%P*BZfO{x67)s4Bc%o(aU5ZZ1k!{l%C}q$#0s$3&!X($lBMmy zd3+)N_HrxZ{*cWwE+gN*w_YP?j=dI^Z;5tE(&cmB%4v8x{n$G-1q=SvgUz7;SNFDF=Nqj02 zM-veeH(8Cxw5_wVe*#E=!yi>HplNURi=lD!{XeT+v%ylPzmBlX#;rYmc*Q@Mi-#y)zPTKQEB`R!`ZEh*Tf%^z%uYRfJQgmg!7ayHq`951 znGat5UVUlWH0P+kW@>wbnEq@H+4-~f-#};OJ_7q>vpWTjUNQwz8W^HJ#22L3hLAN= zsMRu})2(c6MI3d1#}tc0!Ic=Bo%o=zQs>g!ayYyD&e^J(N;d8aL8rs9XjE4nxh5u^Eq)+)PSjyG|j+Is>YDE z41gM8U-Sm~YVHE&s$#LQp&pO?oY)LuVRp~oX-sSQ!c1n_-7URJO*j<0;gEp8V-ZSL z4pASz@VyM@PzgY!mbUl3rVsa@F#LGJ<{mHeV^RQ)*d_C~l$m8xyV(F|wDg1k9jBcw zyHV)t1BrLIRFAWE9K0H@+LBilnKQgVvDF8p%b4Lx0rZ3Q%d^K%iafXnBqJ}wo3jD^ zhSqpqlQ`gd??363aKlr8FL{a?k(M^PQZ|eu+~n+n@=weQV&#boF;K27s1D3&)+Cr| z&-`|c<)cm1gksHs(DsvLMY7DsO+qw?SIi4B`TSq7F<%%W%Pqx3D3=S1rh7C$7TjEV0Qv zC^Gd8;5Ch(T=66JcC}~0`|Zq8Rbo|R^DN*Q=Dtp&-ZlZKT+Qw;ir|qAcC_a7j|ESi z0q()mv5wU6i2}=vwH7Qz={hoG`K@`Jf~cZ-9$2wpuQCf&3?!@! ziX23d41Kaf^BOU8Y@5XWS(xKB*K!2Ss5_i zI`QDOrQ%Lf-B+`O`v2H_`}nAe>wkRq2?D_wA5aw3)q+Mv*bM;#g63&M0wJs_vUVRQ2YD-;eEX}=kEKSIdf*_%$b=pb2mo*3XEKAtNmUil`CAXFE4aJ^L!5i z`gitI^LVn}yFphO+WyS}l*i8H8E#gdpQ2$hmFKayRr@E)GdWqF-2IfN14;Ge>7dTO ze%7`;YX8q!o&w0C_Q^IY&#NeGrt-Y^mMl*J)wOJ&IMh+?Q-t~7Q0>!=r26u7!!%Kz zk55B+3jX%_+NS}ss5~Rh$}@b3@{CpGX`s56<=FytRC%J9rw&z~KJ-0(dHOH{pggBi zc^dwjxjeLLI~$vCpJCM4L7w&c%d`4_M2Tt8x?SCj>Hknw@rCcM1)}-4%>5DF5fjww z?Gw-*ayZ9cK>u0jX-_x-s8~@LR;Yy%UIkjIjo)fF|AQy22(G9zjN}!unT1&o1s5Rd zexxnNEd^ZKqj2h&F4>>3RMgPYg zRssCtZmL5WE?knV_g(MO)a!59rRjeh&GKG1`YC_E9K&RPlS+DDGN~__^an`#UPpg9 zI52T}^&SK(#VAs(jK#!yGkG7omN}->ed|<9Mzj6BVthNsu%cciL((j42 z%1!!3$Q?7T*YQg7d&*CPp>AaS0-lS-$26YPlobcRnwou>8n3UPbp-UJy7(3iA?s(| zeJYCJP(}E7lMFNTZ-Wshx8|@#gP(3EBYV34O*}w@zxO(AdA+ImJ9YjAw|@lXi=Vgvh?7@=F|->%9J&TRlE&k2Jxjv$+lFP(^f?-j zr(m(ArspiDpZq%pS#_?ae;8#U*UD;th&GG!o(Mo3O=muV5%44$dj-GgUkl%-tT^tZ zd33v+1bxuoiLCHbXJg)7k{VC@&-o)nV?Kil_>GXG_H?kJ9&aX7c>FCgh0?6|9@fTR z#oB+gVtVy!V7gUfy1TKn|GhVL@}z0KORjzWtpA<@))r=cHx3p0&%=|n;fCMe;H?~t zn_e8dp>eYU0g8SX^?WQO@CswpNu%m1^b1so;hv2${HE;Qy{Heg3(fPmMXX=0UcY}M&jZyj+n`^z((ioI??xPWIFR~7|59h~ zurTj`^bg4bqvwkLM`6vhFNdkkWU{VrStA(sx_c)KND3OLR6~%7P2ku$MxzOJGs75{y@Kfg%|tMJVGAg zBy#hLCwzymp??{yCVhvm87}Yt)&D1Wn_`T@G&GC@J5TzwL4sbB!TuU5R8B4qUwb9K z6R(q__D@(V?1+%Pihw;vka4h7JW+9b65D^Cmb5)uCSP^KN*!AdAK+(@@h>;u#yqos zl=S>-#*@N22SuCC6xIToT!^W0Xzc599BQCeSU=V7BoBsdIGS%pu@Zgne&w(G|M{Ba zo2mFdI!VV@L44m$y*r;A#W8rI(rfG6WAWoO zI9M@yChQIdASdZE4s^Z4@*}FX(WmI1V2=ayc6sgymhcP;`_=VT4SZDtU)8`@HSkpp zd{qNq)xcLZ@Kp^QNDahe{%~tF5{vr+v7kR56br-4!;zI?XRtjGjK)Keu+XtI2mLW$ z+~2y~cX3;=EvS36(!V^|7WK74N>HDqH5m6T53cgn$3n}R;^AOxt1eMM%JVlj6U*QS z0Cmi3BT~r^86C=3s-lTH3=vq&X7P zB|`R`%=jeGR+ZuNk-W1Yh4}+`|3mS7Qem-0e{%HG8RHRq-q`Q zqAw1mj|Hj8D$CNi20_I>zjkVNLxJG78ff} zosrm#8C3Y#3cVB(-w<383Iu(q_QudMvbU7l%ka1{6l+=Oj|F`zg0WVzCEfED|FUo} z9t!vZ{^n49%r9+C_ngGT0_r3F7~07nSl&z)q;IkG{L=U(Zz#OX7i?@q0ra+I&es|Z z!f5pRrSZj<)u(hw&o`-Kq*WS#uXSZ89%v#aT&emXc~p#Lu}E7OB{DWW=jRKdVuHTb zIGVj>i1g`fne%Ck`CC%rbbPAw=`>2?)SBHl55|Tb8VUG9VOT(16-(6nThVmT5|un- z2Do4X_|_JPJHu_w&CYNn?p%R}r8=dnmi&w#Pg~mp0eFFlP8d`aX@bsH80QS9*EG`s=(Q^NAI;CyTa877$ zrT?Kum(+uL%8VJxWz|TJm3O+cArcOzJ=YAhG-aQu{B&|m`B~2Xd#d41<1o({vf`j?4kKTmnXw z>>J|3RQ;vkyi>#}r;ACcM`h9}CSB$N^55zG#UG6Z!wsU`AHpEyj7OXie0L&y>Ili4 zjV7vpqAD$AKB5Ml3LaK=yihn455a0LkuFWz$2ah|6(5+mfEyOnFE7N$i4Xaz5I%(Y z@L7pZ3?IU%zf&3~KIQln<5P)G6+Rw(%J9LyvV2Zyh?nx2g%4d$dFA0H<^nyy;i5v7Y{ zkY3cbRM(WA(&^>tpE>a?lPo+++6t9JlR8lh@WI?7v({+G#SK4ZAerrGj3&C;v*fF<3lpEb|c-0m#8K?Q$GUB zkXJ6=BhJIei%$nWoABwyr)Mg{;P{j-s6~gbtP%32w8B$5SKU@sEu06*60Vvzzf8L? zonJM70cEKYc<0w>_f?g%8LaZCo0?j$x~V9xPCXK3rTk@nb+xCoR(&Zc=9{YdET&|( zw~{$aW|wL*t4fmj)Oc#EN>u7HPnD-k-JZX&bROT5+VgyiNZU-;8PzrMY4IkS&pjQ(3Jt(&j#?t8sGbFLq2t*;U!c9+?RMwuN3zO5P2BCq zItQ!5m_HP6tqgc-{P^SXgrG(Jg1IQ28el7%@Sq6~XoN{!Y5C{pr{!PSvaCifbl7Ego#g z;-WQJSyesV=UWzTldYBStBM4c`=XKNP+*lO-{Zj#zQx_seKk>E3)bttRx}(I!EwJ_ zx}_RLcL)Ay#2>(-1$J4gCc zjK2rhedZ1BJ>@G_c)T@JGW3_4c>WZ9`J?d|tudPYfncROrMsH)!NO}?KF|KzKvz9z zk0Im_>gk7&fj%?bTMgeQw22Fy0)B?pYR)J%Q7|5i&2S1;By9;LN+T^TZQ)QruG1Cj zY-p3qKc|=<4XOpaGp-uZ>2HJ-r)X(S%5Ej!2D$TsEs@wNqXbPYcVW^Vj7=?B@(YsB zm4c>RnlvG%xRd;8CCHRaCtoQ#h|wa2J2{IErt}`Byo#7I!Maq86cgHKOlXoHQBhgu zt1h1Bkv9vgE6*2gt@Rv7plpps26Z zZmZi`>XBeV>*NNx{Ftm_M39V_Tquedj!=sXwdTm1K#QLVt%61#XQKfujkGm4giniO zNfK_D46DF0ofJpJ$u-G6b&?C_z#P0%Z$qw5!~o!k;sS+^`B&<6X$!6hhT~#MFcxvv z%O%+4=3sbPyh(&2fq1hsucq9CclZ~0luiuJHUn$sFRb-0tW9UEtWH8n#bnR|Cg*IC z2B%wWktI4+EjbZQXlM>9HS%UJDD%{m)>JOh>%pkXP*g{#A~?BWa!$^x3C6G>RW^FM%vu&5vp9ntQMpx-;AR^mvK~}iry>Wr4`VQz$(3_rz{KJJf_l13lrJ(PDc7fjd;=aT?pq}1+2|B;>5oj~$oiFW6bc0sC zjQoxeBIoyz1KJBZ^GG3X-nlQa74-c7+m{eW39%2f4D{Yt_9fPWE`D`iVh5;W*SIC*laq_|ZZ<2ighRjx#PhK_}oSlv`%Ygi+LI>!rf1?W~#JB|}IOc3G>&?=mrss(-aG$Aemt-~SsPS8D|4}o@`Da7AF z2S9Ui!s?BQLR1J_}k4+U*kJ2GCr*owgnH7SLfhGPfJF z474m?h^s(1fo=uOnj*x3 z3R*M``he~L?FTKyi_U9sY<4546XzJm6{221*Me>a?FaokXa!zv&%*&!`kjCV&@u!} z?grfq`UdEzbA&h^$IzQW7lZBs-2+;2t`PY+1iTyco1nGl39$}zE9j%3&LZ%Et_5}C zEb;(oHE3T6){XSN6!i?+k9O{*?~vPz_avIoAG+{*&?xkSUeG+yZoDS733LE7m-+)< zBdP_Bg5C<+4f+P?0H_oFz=hZU8bG6`E=IqNf;NM8fOdd(g5Cn!1-c2;H9C>ELyV^I$HhbU+TXeVeLXcuT4 zv>S9SXfNKE*$LWrQX7tzU(3qKVF%>(TOoeA0v>IJRKN4tS`fDV8`*NiNp?~^@+qp_&XPVH)9>q2Gk#@w+VW}@6~}egZ6>G1KQJqdN~{V z#1e_Ept-HEpF+@f)GMfS720b$MyhJ)Q$SuS9t3$zgJ-g`at0QIhe{eVVq zKtDtKcioss>_NMV?(%lKaGYfn_LU}>E zH=;j-I)99I0ge6?`JapOgT_HSLDzzIfo=fp-h^_V2l@A)9H0X~Lw^D7+YCL6@crk| z6Eydius_NVbUbJ;Xc4HZ8~TBEg0_Q-``{-)qoCcOeV{u)z4wE^752kLqR`Gc?MQS>w71N{WFXDjeB)bHacKWHy# zJ7{MQ>I*db6ztc7bWfu{f<}Lf{tnvt9Qq5WcL(eWv<@`49M3_=gZ6-yfewH!1?_$j z_7Cdpg}$IgFM)41@_QNe3pxNA2hIIG>KD`nx&c(|L_VOspyMl0@BfE>2pW9_@<4k) zw}a;HLc4o9lP0cxKD!)HE((E#Pg?<^n*5zb9vu#Itj%&638rwQ6$)`)F z!z1ye4jPv#8+EH&9>qKX)czcd9~Zj1C`28tgN|^Q`2UYNxz|`1=VX;{6EIux4nAFY zwhL6IDbLAWZ!O6gvySw?#$KG0x7tyb6Sa1Z%*g|Sa+Jt&lx54ZBbi>e6Lh*oS39n; zud}VUQvH%{??AVogW9iDy5-@%0lEpimqM=D{(az5zp@=S6<+~L!c4#ubGqen9`{y+oq%+HS z12VR2GVT~!#$}K(?QzzFbY>Y&n-I@J#teM7|45Tjt(B!rUzQ%oxKoosI5ULSVoob?3jK{~UHO^`wB7}+kr8d^pLWL&JtAe~vpZphf8$#_hYG1v~NUDiRy z>zWMGnPs@_LOh_!ct(?9UY1_SD1DO4LOQdIIAnBdGM>|9nCo#gbl$AVAe~tT{d=y> znvCBMEn_icjP7AQNN1L@7cw?!GX5~Mj17=+g(ib^W*J2eAvS6<-X2;;A7uPZlR-M; zD#K?DWRQO%JGQ?E9RBGB@=xn+B{|M@>9^9`+D0ARyjss`XREO+Lw45)PASZr3&}C!?3=Fuade@eM6TU zNtYW)m-Y6NoV<0U%QeG_a|%}vFU#q)SbsV)rw|Zy0!vaSPd52Ox^z1IIh@j|)3SQ_ zHN)09*4uBe-3U{WdKG}?YkCDzdToMUdo{htw|)-2$i6q=vrfzB0+kQtojV-<5AlHx zYKvCj9vVwZ4;wb`M%xYc^^SGJRu8v6JSq2zkrObsA0J%r*+-1 z^^P0tH^Kyx<$MA8KZ)=5DJp+z!%oT{@dw$@=L45xg?aySp-w8FX1hKwC1WRK^lCDw zY(vP1L&p7@jM*s}W3u3XA)|!!^Jy~9*Zf|k-tTRJjIqzK9;7oaD$7#H*rn;QOp{^W z&-X&c8chbb2g&GyjAVU8G#TdgQG{~;Lz6)|<02XFKt{4YRt_y=4P-2Omdipq<08Ar z8=>kWN_Ii^XJ!|hCgoZ^$}rA{VUV4TgJJ&)@?<-c{sj~!3(B<<_x6V&3m4%>9|k{wSUA~_sL08^ASX+fZ6fdr#30aR=a!dJAh?DKdtUbzdm4g z1GWd40G6XmOZyOeAy1Eu!tNsGNx?(^J$(7f>Iao$RBP$vN0unuE>B@!F=KUlkTV zljADehFRDSOv9eX-0%go*^5|U!f((zcqwFVf_;x-nS>M~k)yFEKzkTpqkM5uTjc@U4XlI!X{(n?J0}0O6L~qY*RdY=_B#=9 zT$0RBuJ_Amy*~$r>J&L`-8QCH;}dvdrTEW*}!ar z4b09KUSzciX{)gR^e-w;0>S1C}6^8M%b;u_5vecNf%+8fqi1YdVuW#hG9`&b@;mz810R%=5m{_r^?iN%G;1(xDA`) z)Hg|&LNwYq?5Dw{$cwNlU}J!x>XKL;u<^j0IxG&1_T;p(x$tZ)u!+E4CO%o$l}Xzn zAM8hf4>MD>{47=dl=j?qA>XgEFh5X!Os*w8$+aYe@mjJx$7@A5C@g~; zD>iXsRoUOnPzBa&GCV-vK(@7L;3)PMLnieP7ocCy&d%|!GL_}Yo@a_zW?R2y`oNPd z*Y2;F&N~lfpiAe6Zu6!5kXN^~ zm-8iRzn>sxBFEBC3uIa}gIJ0^&VK0PC1hnz?m~D>!s(2F6ML@I4`_Z>37qVQ_D1u7 z>ustF*mxaA{`LV*e+0ELY(;P1vVK!-T!uovi24Fz2p3?as6z5i$l8Pb-fYOS&xY<& zUKNdfUb2~TYA+AjjO;I{G}}h>ZQgnj6O~*i@J}FjFXUcC za>oIe^E2y`kvUnF*_fWm_8$X#2__OWJ*A7VE?@=wQhN(cz{-HRfPIH-d}ls7T&DPy zHKepm28qhEi-DBt!bauU)-7^-;Q1FSDCCulTB)@x>7nZfH(`wBy^3^Ztv!nnkCk;g z7kc6H;U=_el zBtIb6t`V41SvExzFp0D+KpkQ-T8_WP+2zW{29Tz}kx*kJl96mX&OCGvOO=c2it^hE zjMnh>7m*vvwT{-U$vvnd>&5y#C=84y^EdyJRJ#6AS(#nPR_IK zpOP$DPV{Onrxn8xZ>v>gQ;B(DhA@q+yCS45$Vw!Tjl4+TT3|)M^krxQR;A-B z1K%iMi-Bz<-+R+!nYVt8fRX7M?31BW&d#Z`zC3uWnqxXv{bO;e&g=9YTI-{0v1bOi zFYIR`Ph6yP)(KdT;XKTV5>dXH&b+Jz_5;EU^+KcP8h!io`qGOudyqyO!!Umrdr;0g zoOzf;<$Q9Il%*fD3^8sL)>dZT{pZnA$7JmBWRuPAK;C&cOK~abEBgc7{%RXmI6l>g zXz-HkOOyAQMu`rv+EM5z(w#*8NUr_loXOCCweindMcp^UoT(!7oat^;&1u2rM7C?8 z!;TTym&U!aQ4BH|&S>e5W|r?jZ3a(2&WN0hJ}JI#GVZ7L3FhP!!B$ObYIe5OZ-yh- zx#Rg4UaZI|N_Us-PQJi)mTW(h^`zHYg0V%vo0?E4<0Z|F1l&>1Ng&SuH+?q1+#J~mbMGIW*!YNJ7O9@wbK-0ty(DShhnrPun) zEjZsQ{c(lT$qSvfL#LUe5{V1oknM$OI$?KNzi(5aVg?;n+nF}gCZQ#T*_bv#r&otn zuNe4Aw*q`lgl;s;u)lOB{;K`01}AQgNF-jS_Ie69>T8f+p>;Rrbh6Qmb6{vh^0T1W zM-Oq?Z&UIMfft;Nesd(wir}7PucvV#eVyyI-eH&@VO~vsCW^G{qqQ(y& z=5?laC-suP0ew!c?oE1KX|^q9?$OG!ZCj}*Xb!^!y*QitP8afTI2PxMa8LQKL-O&2|HnpgO}OJUP8pbEqCnpvOVJhvfGt`DAC8LH_pR6Nw|ymiAvM`8U#h z&wQ_SPEL=-Hp_0>IEZeV_SuWiPnEeR(>!$B1H3m=qjaNwu@)6b?WfKCN#BXU3Y|C` zMfGuxrmtaaO8LFnr0-qk#w7a%_2~!jX-EE)r~O~b8`7FGYXahX%Myw6ke8{Q%Y2e! z;6tXhf?R04cn5m-U6e@NVMQLdArEOs&|B{#NZZQ{X2fgw@zACB6ujd~c6f{p5bucfOKa0c;FzH=KsK8`ar)N-p~^ z8Ml#hVL4_|o7)aE>HN}G2bp+azxd^)SV{q#hFe)84+Tj0`v8RW|{l;w0;tgj5V+hWu9 zpOw>cW&P*4o%YKXryEQxbMoRv6$RCCC z-y=x=-2KYmkx709eDd!!`QuXZ=?6--LjJ#@gMHL~7oN`3`~=M87E{<98U z{&vVO(d5%0fQ!bSO^`nm=k!moqy9eu6Y6J(eQR_WtL-N9`7OM<@>TDQ1Vvs^e7C;{ znYd^iT7&7KXl-C)lyhdT!&@?TP zc%Je){=o9eGS7>)9A#b=qp&BS<@Fhi9~aeUJM!8w1Lx&&Z~q5y!G5z=NI0_cJQvbt+$;+o$#<^m7Z?QP{kU zNjsu*2;_gI9o4J6%8=L6vrwOS$3yz01u8G|d4oLHm(eGYAKwc3a!mbNO8!>JA5+Bg zk2-YuH08KmlTRv$YwX zt;Mq)8J=y#v)uULa~q^f51ut-c=iUKZOQQL6FeK$HdrS4yrWUzp1MRr{^u*aQX$Wy zmaMbQU2V4=VNvG|sV!(5VKHLMmE_xOPVTSOnb(t@R@+JDW277_;9Er}@~&;fdrG)h zwkg-}sX4G(!*g^6zCn5G^6a|wl`z%K8<6!0WHpj3oZnk*wa&wK(=%20x)#r>u-=X?e{TwT&}(HI-&_)=){x&qVcZhU^L^EH?gfU*cR)`-ccF;(`f^ zbqHi-h45~YL=@Hq%nodX4x|60Ds+5Qp2fiSV*O12@yz~X$V~ap4CS#tlEHn*adanS zo(mmmT(e)KbfkQGkk3ZseI(_xW+b9#m(0kVhBJdC9g}t@6vkk~cwoOGUpQTgP`9xV~ zg$Iz&e%gliZ1S`^`wU-jp(vtY8sdWOh}EFZsJ`^)<*ePwEuuNu^v;%gB}>{jy~~+( zG2Uq-LAXd_2e9$LJ|dtYC-)n2v(9=S9j>Ya)P?8vSRzr*dFkV#a&KPFo!HkGSs#O` z!9y5AiSx6qWf^Rc;)?dOF;7DtV>yrYbY5Z|?VIpkJMDM>0-*|hY#%cWs*56|9gn)W z3-?qP91BjR#ip|L%JigU6YWU10sj7zWIFgVeS8R|ux>VlM=Iz&H`={U?z+?Vi73Kn zG2~Uj*MER}``2OY(vD^95PQt)?5Vvp>qV+0N#B`_8w{YEHEvetknDMofs6VG{d@9( zl?in=5OVcxklMSdR_BT8Y;(;0q$iu^QBlac1$y2~!V#m@@1xLB&7?oa>vY(W!9KK_ zWzX4$M>}vn@cEpqIwY-toV}2<0r&Pba3Xj|P~UDc#zb{kN~L`URi+a#7l*df;hmPBIqs_lZ0LC9K0|K$Bvl+BFOS2agUm?k(7--F$JQOh5 zm^21-LiQfWj*@I#b@*%q=Eb|sThZ?+zZ)n9u8-Nsn2N0m^`3{#mt@1@Xu~!@`T+6V zY-^(dD$Xu~*enE#HsgKiS8%WLm`8b3C2dFMfuN<;x_0oKurhlLl2t)Q1>UuOhGfjn z>9bf7CdJmf7hjw3Y%iXTAC505gSicHKi=Z3%IP}YR%O1&A?NnghqglIuGQGL=X&9F zYHGZzOrG_coimVYyUyG&Wh|TYr{Bidbaf)}Anuj^o235KE_rxXbxrzy`%GXJI*k08 z7g!lE#H{czCwDm{c(T=*=c55d@jQ;_GSY+R7sAm|S=Ity2fT;MlCh7KQXiwpsqNQ8 zlqC-$=yyH#u1zHTxTm?#HROxs*=l;Z3Qn2!RiD`=`?q~2e8_d!|26Tkss63`*tn^S zr9E17N#TX8qraUH>7`6GDgk9+HBqDli`Jojh|D- zEs)__f9QIUE$@Pit&mYn^ow2j78vW-1>Hh^c9+(ukiG|JtX9K8{3K$QyTxoFQD z5f36ca!f&c@>#;!G&hp*V(VS1MU$>a_EEA2`W=A>kpALK_An=>*FuL((wt?vJkLPC zHSkF%p9zp{iWcN^DL5LyjaiwEL!5kHCe!Uiy1JVVKHaGEFh@MdbX3MNq;uV@p94sn zPt5_y0O=$eoO9CV;dQ8fJCJrG(hkvIHaRPE`kdBXhB{JbvU-qy&vz0DOp}sV;*OIo zjw;9FmJxPgyWD!Y<#KU3J=}r$VGlkJ;FGw+Z8>_b<4%iM>A2n^Znt1P{R_*97);ZW zh+Dx+zs=Dh(;R1Ulsk4<95x}PLS{Sg2KnqP%b3$0w^>9nBwg&d+9GZtnVaZOid|HI zw}55Ya~!AYM2U&O#~g&Ly_kUY2acZ=jleHrxX2EO4SWOabIS(7!!F-hN! z4ZytzA*%qqPED3UuQ9;w2O+B;>v}p8Ds93bYX|Tx^4Wpe#0KzEoWRV!I)FDHge)(3 zXKJzxdKCb79)v9V&7A?nUd;6B2fkB2JFqft0WZau%w%lAeRrSBkrmPd`w- zd%(Y0ra_&+qkm`}evj4h)}T7M6+CM+*|Qw?T0|`>0+slEi}(pu$D@PmqyhY1nP#w` zsQ_LepB zaPL9LDgdujlV$J?V}RQaLRLTaXm;Ww%V?0b1NavC?7-@teoJbtn4Pn2n3BgbM^p_?O&2c9dR9hjc<+gE+~P?;Uoj;;Fn$u8i} z%4Y{AYZG{HVOhp8avgBmM?6q_TMAw}GAY}_P{tzQ6AwaGE_nBznUs|_-tGb3C!ZZy z8J`7jwIyvxTtZ1>bJ$5m8$lJ;$-xB4ThxKejk}EaEZCamhKS=G%M0yOsD87RNb` zZ&~QZo{J;|l+Ib71`I2?#o6nBb0c$A9o$4f5+l@!6xptI-a(PyKHp- zxJ~icPgZpF0#KCsQ>%E*dIU=Jp_UCZ9Cx+-1EYwTatobpN0}H)wsw*Dc=rERKyM1;+PVM~c5#rtKLa{$QQ9 zZlw63&CxMZY_L1JM_Qh9tg`_6>o5l%-#&bu1yA12Is?$JMx25BzmIU>{(}*eg4V6L z5_cSCu@@m~ll*?V<)rhW)#Z-gS}o5~sb04_-nZJI*$-{v=cE}}rTL-0j$Q27X{A-x zcB}Y5ivy9o>#dG%t1RT1U>>;;8G?w9hB@%FC-`R1R#~3#gk|)0yVztq z_D^=P*XDS|E}pSFfUI$_PE*lkp!L00ajn&Hr&WB{>UdabJ@r62#!iI_4UQMB;vtLU z8LPNciuk#e5YnYk3HpNB&P7u-Px!zpelI)BlUD99Kqynzer!3|;`Hb-i{m+~_^H+L zq*Xj9nRRTV=R(KDj!!M(4vXVjtN0iOEggnyoa229ewXzbizp@Y{}$Ql6Bf+bzGHR# z+D7uW+Qg$)$7fdYi52m02pxSMc&bRijTXlT78ofD(`sULF&L^HpIO9Bn3q|^-z;>m ztE$6G!EmMHPZlzz_rBamrv&dYhI+jc97==xtqePcgUpIm*=&ljs0qeZ)qGviBFO3l2ALjV=2)xYX zcw~gwKHTy15h9W00H;nQC;v|tCBI1~zgi}LN+y4Bgt%_FtJKJr?hO&lHq(u zhm~S4do1Gb7P=<}?qvH_uV5a|ja+fROtn4TpV(z}++!El+Z<2X#d9{tgLZM1o$kM5r+ea_uF85u&8&Y~ z#9bCg!XnjL|4f*g`pJSIzynwlYP26pD z?68T)Y;;fQ3qEhohnG3twuoiRUbTpqskf8R6BQjf&0^9uaSbETG|IGtX(!Vzrrk_?nD#R5V>-Z8+|T(l zbux7^En@0rTE{fXw1a6U(=MjnOnaF2GVNnJz*PK-^JnU0>S9{N)XTJvX_RRP(@v&c zOuL!(Fzsd9$8>S9{N)XTJvX_RRP(@v&cOuL!(Fzsd9$8>SXF-TEx`Lw2o<%X$R9z zrd>?Cnf5U4W!lGdfT?(x^JnU0>S9{N)XTJvX_RRP(@v&cOuL!(Fzsd9$8>SbETG|IGtX(!Vz zrrk_?nD#R5V>-Z8Jj(eqbux7^En@0rTE{fXw1a6U(=MjnOnaF2GVNnJz*IcO`7?Dg zbules>SbETG|IGtX(!Vzrrk_?nD#R5V>-Z8Y~}o!I+?nd7BTfQtz#Nx+QGDwX&2LO zraer1nf5UqU@9Kx{FyqLx|kL*^)jtv8fDtSw3BHU({83cOnaI3F&$tkp5XkMI+?nd z7BTfQtz#Nx+QGDwX&2LOraerX5NrV(R3VvWuCXDYWzotfnkFiz$T6q+vVYBy%ehFqr`^u&G?TN z+WJGE!X`#%z>i6;N7DI^6-~TONyo?Fe{GrNdz|RG!VEt?x&BC(Ggg?{y$x@S^qBMq zn>azAT;AVh$GHjdVZ!);YS$`K?XsI0j%B?01qJ+^@pBkI?!|Qc8;ox=;O&gJ_NMc1 zVBGmqI!@+6`aEyI|I7F#FQ@a*SX}?d$=QF<3fUjWuTRYSFZ)JSa|E1#(G5!Yw z{vzXRUrFcxE8|mNO~(LLn~f4LenSCT`|f4@eFJ_X7RV&$jyKc!&tbgckLmal#{Xr&+Zez5 zPwD)fjL+{=xTfd*jOYA0o&Pz;A2Z;8V!Y|CbpB5nAN_VZeiZrzmFsB(?q)plPC9=L z_(gJx($yZw7ov2K*lx@SMZU^_iRj_h!H^&Vb*X0smD7d?#>= z7%Msr_Sw(;y@xA1)WYpQKmd^H^P7KD6*-#g^C`yT2K=v#f7^i1!eou)bQ$pP0H^-) zv_YR9=6{I!Cvv{@+x{fyEygPuABT=W_{9EnJ!dgqV!(ZjFE-#eGahFAYL@>P<9irC zmGO5OKmJ_>oX$A?wm#`o#`rOemomPDaZOGG;~fV4M#g`__!O42h4I%7{I4@E{;n9? zn19-lN}m;smowhR_*TX}jQ@~v>w5~&{M+M<7ch?E%j+%1%NU=*xC4%Y>fs6lKAG_? zjL%~J2F5=&;NNAuV7Fq__@8Aw#<-U69>zBt@RN>K`uvsgkJv$%GrsHnbon91kNF@S zzn1Y@15W=xoXUH%0e_P5UIYFX<2eKA@-66SB&Uk;sCq1xFz#Hg6sOX}*ciKKB#LDZ363-RdexRRm>+!^&i+@6|+AGfZD#o>a!Ig~vQ{(6T zL5=?+G=$ka{|exwf7eDOU;dT_WZA~y|F+9tI8cJC)MGIYIPt&3^>8ldyOQx!{-uD4 zjK9KoHRF>QKl=nF=Q0C+C*wb3d^+>*WBeV1{0mQ1a&kXbjGFu>fSZ-~72q^p^l`gn zv3%P}3KwfJ(Z#ii@kzj`yyvp~uQ9%e@p{HbGkzm*vwUx7e(`T+WU_&AmjVAJ{%|MB8N8l76}XxH6M<7bbmIsgUEh;`@veqb;ofHz zuJ!Bl8Sh~nCpqQSz<388*v0jV{EK6HP9>*qyTYGgd;@SQ?s7e6?Efd@9WN{VLGaR*dosx%D{f_b7^Ps5sU62j{v*Y$CUFC^dOHLB_>tW~ zafw-+Z@a+`7cy>K*Vi+?*1&%SaH~n2WDWCo82C?YAbw1iySboDHc0-#afu(ZoJ}l8 zOLZUPQG@)KBn~Ik!41v{{s#O=*iR4r=pnA#7$1I$5T^k5b}C%+pQi#R{%-n_4qRHl z^GKW%>&Q1*P7f{2aV_L$*Jj}VK?eM>4ETV=F*%h#s*Qj1tg;cuLNU@mx=sOLlUEEl z>EE?aeW(B1N!LxlPliA5LMzaPnv&NqnZIwVV*G^h!^V^K48970ll-D-iXU!VUh_2k z5ryNcyyA@4eWt!^`rM-7pDJ9d|A#c3T z{Zz)C6BWN!#%kc&y%fZMzQz1?Cn$alEAsj&+{OGa0;l#G*r53D zS413#gjAn>|51!uJ-C4re;v24gXLT=`T2{E{4N7N>@@RoIf0Y>zNeHTOvF9FNgwCl zbbTIYylau-Uu#p(-;}sc5x)IMSYdLVzr}GBIul1vKk{|Z`mcno0 zr1xZy^IPWc7^nDOW`6o9VY7Vcci5=B18&8?S`kq#@xgljkomi;icxE?ZH&9v&#dR9 ze_`Coc0Pmgk!P6e->pqcRHR6@C-r z+a=ClbmaXE_^8QBPR9{S4#ltOn#j0oi2_O)p97rq8Q^}Z>9d^q>yB59SxSt!Uh?x7 z9r>x`*8zO{SO)w>mhYUQHB;$Sbk7jUb3*=i=P0O{;E##U#f_B3Ak(*wr8zf{weu6v5wGt`$y`&Yq#{f z03^ST{lI8`a53YZ>>yp7@7;_SasSoq`7_|8znAx$wEj2_hHaLwTjH?i`;_2!SpM}H z_#a~a=r0tbmhZcacmGV`{j9%zsw!9SWQEKBNrY!jjMu%X@M7kVG2Y=-IQ=F+UG!TU zX8KPEDEVElD1N_U7hNpJX;XL!<6AU*nZmVYQZM5@;}uT-_l&Mj8SiF2E7fB$Vw%dg zJFWowtzNp8NnG9Q|E^~Kz6TV)HZJ^$an~4y)BjbZ%XK!_+gt@`c6gP<6_5V!y}+s6 zyLkUm>lbgcoSw&&oUtrF3j)dBdaqW%2*$@tTrXMO7iGZffm6O)4Ec6s;QzkFk?&HK zp*EiW2KW(!vlW)Se^34nU`at69 zUjO&_5_3HZGT;|yz}IHLU(0}>S8DE0E&wj|uTuK+^h?|Xoa(K^VwLLGav|OYPW5Aq zFJ+Y}yw1=rZs2D9!Xt6i^D!#ln^=CF@t!{@{AtFo&miX~z|H*RpENoDRswdgoB_rM z4E}bwN7aLq?O(HpiH!H{RRT0SER{G5)saQaZ;Z3GG2Y4jNE;ttWW3{eRbDOMfeid} z%FXNZhZ*n}B#u$&EhS$Y@BWg3|I-ZkwAreDI(R%iL79bEAaPv<@a=l$H?BwSV>}n{ zvC_rEgLq8Cd7SdHoHrPE{#OB7{eKFa>~kQYoL!+dhNyO;4*jQ8=v0&Z4bw@RG9=*Z82Q$2Tb{p9kqH(5@%p??0!crULv-?Xa))$^1- zomZvTTZ_bX8S?Gb%-@x#__cgDGCuIO!nJwA6O3yC_@~rk@wW`}KhyYwijl)lVr;e2 zGq+LU+I*});)C_9$bdIwz^?;N?cVc4C13u(G>Cmg@?&1g?WpyaPk@_^3&+ei$L9j4 z{?hlFl7B3#yo7P*w-m0;V=nxYqA}!2Dj0AD+l^o&YZAjqI-`GyWm-8{@0F zaDb;UkDg2cZZ@7a0hjj1ag{ty^f=>rhIqvvq#TUD6P5n-f0XI6V?t{te^CbfTM~yH zwyV3880_b%`gE}!Uc~r)z~y?@5dXtiWDS9Dud1!(i+(J<^I2dg8#nE{Uim;U*?s{T(_ zMBJW%e`^N(6^Wxi^8B!Zc4sg=PX;Jn7GnUuQ5jRA8;BU<2~H3xf#Tzj62x@|C{-@oUGbC&!Eq3%-=Ch$=B@pe#W~D z^ZBQNlij+WQv7$Z{Iiyjo`bKI5{Lgc#JgV7@KcqXZJhKU8RQ)H4RiUYNF3`d!}>HI zxS3roWd1tCI{k_a@^5E;F-+;Xo%Otj@t!jkzLfFjGsyV}IMrJp&!3NF{t6@{`{`qQ z)^rX7m*ec8l>Cbo5my1f3;es--f~&a&m}*9(UBK`Ych4P7?3#H(cs^L7b-oAc>R7D zC;Bbp#(l2w-&A_`a{c5p|2d3z9p zAI6K$RXF|sZn`3M+%Bx3{9o|Ep9F5!?_SA(e<*RR3k>nSYiFo-?BjmaV})>BrGAyK zaoyx&yu+~HbSdMm8asHwsGnl{QFG|idepZ%&Kac@modLfOIQ3KGd5o8VQ$5#lJs;2c z4lsWY`?nm%=LFTbBMkGgi-4Qi?G?Z&-)>&NX#VGI$q#=uMwRzpiim0Cc z=L4sCPLW}}i!#5sQVG!3UmF-V?)N<;aTcm0uQGr15hdp%e)cKj&WjYTjlZLpDSdXa zKH9qD9N=d45Rf?LpR1Id4wnBUyuz@EbAU zQvY8ner>k1&`4{Flp4+KdZr+X;0ymTMy$t;K04F;!`sd#H)c+4MejYeW|UmzCr$Agmsk>&;+x-1y? zHHKm>EB&#cZ$&WH8j6ICJb~t*Kjw@3TbKJTZVR>zlCjdiJlGcXwIYY$pu{cyW#M2v z6z~Q7&7u03KW>siE0kCsTvZ?O#~OU~{=o9)(6XkuuRca39BgehYLu2hGKa#;e8I-X zU?6^g%&pO2uwh8Kv1RosWsICjbtA35mPkX;*Sazk4>TFsSkWPhqAJ(l+)OnvNO$!V zPmF?N!Nypyb#Q*o!C=(L+Yo9EM8aXz&EOI@p=smwunc1&(wv%NSuD~PMyrJ4E&f50 zwc>@)%BX@kOsr);f)0YUG3IZ{Bvn<6PQjs=NzLXU3NfTX5eIrlBxWoDDw+kDJ`Obb z!zevjfiWdNilTOmk4R+L?Q2~g3TLR?fIsY884C^Wv#4cL%Z&x8qgR$0v&w{%3hG%+ z=#oJS#{4UNO`!%~v%fX&3&vuD>jie@uWt_eB5m<#o2dac1XqLtLDXuvF|%g+p!cxz?A1Gxdn4S5ze*Vh#IJU-8K-#lL^ydttZ2w64p z>AvFdDqn*??w?N#tZ_j~W8@HYPw|z2_*(p7m_n@bOI6dXUVI+63wm?@o&|cn!C&D+ z$qRk+lzP5MG^iXkN>ls)gR%!-iTU|yZC%;2tVTMc&(qvrW_>0(OEE)@`YNW&6U+PzCvH6bO*7@n)uXsTeLY? zIcG_=ds@;F)nEn#-{sC{hj{@xb5;mnZ7@Ug8ea^y&r~xDf?rXx<3Zj{uuUG zih})?qHsT1Oxa&4rnpTdRJjY%=U%E`AEJ$Z8Dr6bnyEWAT`O4?50U@ORDFZsvBzew0PX^hG3(=tvOEhLG5bJI~~(! zRa>4GT`f#a4mxFW>W!*gFxNem=J7li4}}{enj+P1BYy#=e(-sj8KwrBBdr*`GIJEP z2Ez?F~@@(2s%XgpRqsC7Kaf6q zOa59!q0XlulsQwribd7PP?R~#bS)xPnhHpna!x_J#OHEYiq?@b&{UF)A$U0Yf9$F=<-4><)%==yh)~qX zFqNrjH>$<4+ltn;RX=TI%D6*tgBh;|q<6`9!7*rgS^gqT`CMp?#I+2xS7F=*qAxP7rBaJ6UrEd zj3UY{1Cg~@+<)1t@!FdIMILFsiZ-w92972#j z?qB9Rl3xB7q5l+jGK11cOG{fg6p(wDUvxhCscC*~s3nNK zlrJC$(`JxSOTF+fwi)IHTOzSldikHb8RA->s+VzrXe83izIpbd^d-N1?#1)M5G>yx z4q$!YDWC)}^Mhbbiz6h3p6w4@+!l%j^((&A_@v!s9I5v=`x>yX)#~%NwTnOmk>((6-WIy1xy_ld zi60L6{IQsSl`j~M$5sioA==Q^(y|Ikbl=eXL-90Lga@EeTfDK5Jk(0H119Kb00j=z z7NM^m@rYIlhJ)?uNn0yMY>_O7bbPdz=3CJiiM9CSsYhHU#la8d#$wt7Bo*qIf<9W| z13!Nkh&EyyjkH>UPZXcVSfs_*7F>Zs`=b7MQ!38}wfUHqr#~9fPC1}b$`=&RLj=CM z%!hqD-|XszK2HUM6=e%V*#(oZRGCD3VAwCK_c!>kO&DbpTjPUq)>y01 zU1VPgET0mNtn{o%Q)iNUI%#wO@iJmNxT)Q|!C{;76)QYm88OEOPHMLx>9c&8E7O?{ z>=U7v@ZkvLTw9|X8q<9;>7{RoqyusnqDL1iCsYoDCuc*OxZ30!yHIYI@aa1qr& zeR2byk2&a`qjuDppY%gC=^D6c^|FAEHh7wJ$s2-j7!i2447G+>B~G`f9UOhNrUK3G zWcL^xYIGNbS}7ix4C1LBI_U}M%s?oD^8teo45Zg5>6XDDn1LU{`<+EQI1A_@zte3_{eSZ=|%-TPlbADZZL0 zPHo^nZKzNm(np*9f#BefA6c5q=c!7c=vGAn%YD&Eb11ONlaKAqS-SZ?iv66Oo1SQ1c)e z`L5*TpH~;O<)D;2*nL%m=3CM4JT2*i=cIi1phnmEO8s#*%6AC=r4z$Lj&iDL=NAu6 zcRK6jk77%xK|0E`zLQ=6%9#$(rH80O1kaOHC5>!A@jKMfJ&vdD}ES!GJ1dn!)dRfHO>ps;mvua21}A%M$4UXaP}F zgo)(BGS4EPyKo9*)h^2DJ#+@H9c82vWp;DY-JrTLc{J7?x^#oU#cZeyscndU?2Ws0vk%`_&-xo1s$h@o@P1pJ`J&cPE%_tj%v z9ma8U9LFvy!cwKUq|)b}L^fvbMg27@kMvGr*h!RyNQJc0=j}rlp@7bYQte6Qup*Sx zuynhGIW`9k>z|fjOQ0p1&X=hObaFtJZ;*`@${D3U5U(_?U(8c*h^M6{Uf9SdY%`s# z^@J#ch$L0Cjk|u`FAoOFekNz;bV@TBZ!!7;c(Ovo$s4dR!nyJl zeoRVnnBDEdIw91!%7=KIVf3HmM(i!qat_?mQKc0{P7BJ^L7WWxW$EoEw_~wE&6}7t za*+eUhhj5Lvbl7XWeCvHe8hbDkl9xs`t*v1D6(B`x&U{#EZ(HHBM_oW8%tF!n`}+E zr_x~^c2w1H|EW2DYT-;XQT=8INy|5%_q(y-N`1F+73N$Xnwt$-*Cx{h4B?n7c;o_J zo{Xa01`pM8LIQ^l-O?R;y4o~>mamwQl$3i+9hi{PQ_aJsW$s~VxkabBR8%xg;1|6a zC!$>8DtMb-M5_MOg4AGyhi*``*(9qYRZpq;IK&>(3B8O0@uw_{?`8kUE98-0PM<*CL~ z5#EMo?+V&zozC$SpJu_y7;tT6=9&8nO>|1EjQr}&d;jOGpgs*e?x`yJqbBc}!bhBw zrk)%PmbKA%$-6Uy2Ldes#BULq26qC)_bmG(3h}SMrcHUrva(Mp(ODHh*>$Ykdkl(L6? zBwN6puU_orh!%7_=+jF3+cr}#B!4a=ked?UsdUD3%c+BI``iHKF&SoJLlY=-GIp0YHuu@-nrzo z8UO}$P9F9CL4QF~D7xnuVu3(6?v5b!uZ%*t-H&#g zJG-qe4O>-B?0z2H`Wwx`?u!E1vpELs!d}V@kaH8O1in?k863cq%r-!!Ei4rj-y0qRP&UwDOwhyRi(ccI2J@3W}tEhhhWK>-%Bjev@0f7 zYx9@snO1n_^-VKHpt5ae`wj7{cN@fXfPVP+)|Dy!OoYvrb5Sv(V>XS7scVoa{DCbd_MlujeT%4~c<~|lw@9W*~q90)s3AOZy zrg)DXPAXb!&JjiV$M}J)x&_GagPXOwcrH|cR}V&uo>xc#Xo+ms#NH0w=Yn0gJSiG)XyGg=s3Nm|f_jUCZBodpUl|XH@WN7(Cxlwhq zu+6lcqKHkZNj1asPGd{L$a#rFtTyq3AIJ#qE9Kg^x`wpAu?vrP_#28Q2**15vH}N$ z<0JQ+VAboxdJA;(BN^4H&vq`qWk?(CQnO|;*v4)W4PFqowE0AoY(wW5VaNLmxTQm1 zQUB2Fscx>Kjw418dB_s}ICBEU#?v{7rs4_SB3cHb+!}X)D5>A|9Kgj9V-yznT|f5T zHLGAliBnLhBZvOfTOVyV;arYs{bg8@}+3>ba+_<3=sK}_QAYd5y{L9-!>>JHAw zO{OXORoqTo^C<5^%* zhe|Ws*P^AD!d8R~B_5Kwun+BFIAMR7CUEsRfn*CV z%odJb<5U98X`!!j;;QFDp>MSQ3Z+w!yU~GDqKx^}IeJ)&uhs;_UNardi)S&r&M4+X z0jQiQr;m25Z$j{)PNxVCt@d9+X?YKy_%UCtF^AIy5Dk9WEx~Bb@Cw z!G^oE6ensI5jBfcSCDG;+IS2yMNyrm2iHhW2XI6Slq2hf5@m}6Qrsj!Hy#1j$A{Rq zG4#mu{f&%&05k61WtOkMQyurXL4axntv?+g=%^~9zInJegx12If$N=ny>DKEE>lcn zeW(L@sc2VN5D+J9_`gU|*dd5fU`f?XJFbgS=1#Z-sXXpV1jVd#Dbkl+rJ*}`&EvKc zd~0Oqq3vq(#KXANj6yUhYg>9@lPjn@X1pL^UBfWdO?b8E!`-tHKMqQbgfM7>I7dmQ zeOsYw=ofbLge`c8VAN$Ui5`ezZC`PU+vKzokLYzn)j4aSu5AemNTnlNb|M%u72HmY zfR-&zJ2cufgA)sV2!^T2^p5jzS((R2w+-+A-4uQ;Mxe(-hvAQU05qIv@xiQ2b|;6_SfNTw$QqAES|Mo+yEE<|G=XS z_HO5PYdSXq1;S&@v$z@h?>*~T;RiA5@|1^spUUcx$e>nLcvGm+w! z+0VkXVP)IpT(;(d7%`A--7!$+`!kUcVN-C83=T4tCM5JtKUnE?4HS=+T%m9*R+h?gD%B zz3cSteX(+HItuGB`6BjF;v|=@N9sa1fy15TH{G9Yr@m!!x!IcU?(g5o-cD*+?ZxBT z-6Y3~4Z#tdn!M-^VV!HB^3+RLs2BM53D^UGn?PKl7di_KZ*i(S*f48oOC54jMHNy@ z-X;Q97zB7&G2Yx|_6i?*!)6{)h2;7iFd^Au7~hGYYU^a<^?(;g}| zodHIMkye<$AUlq4m*H9q2X4VmR~or$IE^KPu665pKJa0QEk6N)22Rws3u4Pipp`HV z7w>1szADvwXRE+-+Q=PVOO#h34dsOK9_{|aXTl@##*lo6tILI1nXG_zfjM$9r_&M? z>tfKR(diNTtumL>Ny&zNCqR z{Q$-u`nl}mVuC;z0}iv)$EwuIO^ZoHyjFqMn}Zt@1cCwPgCAL4+*1SCq>0v!x)f?( zB4Ymq*k}qC!96P~oPN6^<4`(SK{lvt+A6$Tw93%*>^#|UNqD;Wl1uxeey2HT#G$u) zETH>N;&?$9gUzZp-qPuO!jcbjr&be8uAqA#-)&7I#d0Ah_K-~xlfV$43XqOjF=Va>reWaS2 z&wG85$@)0#-MkKW1*M3^JCA4<`*uMENWpSEMk>0ejI+;6`VksXKAi^RMCj-;W+mz5Hj4&eK1CPzjtaFaHJH z{xJTW{y$IY^Pc|mSv`Lr5`NLp=|8ELd;I=2i*))2_F+;_|8&6~v_!v(7_Q+n$-PHW z{eSsAz31s0_C8*JFUQk8JpSK^{+aJ9y{C7rK-T|B{CWNLsb2382h7{^`_)~&|1i2QmGc z(R=!qf0)$>{QJ?K^1H|}{X2iq2R-%Yj2vHr)t{$7#c53c!00`_x1{`^XY_afqWODz z#=ifmS9@*we*Wu>{`dBMTu-h0{Bh~)_5=sNF?#R+-b20(ewIG`gr55UH9xTQcSuR$ z^beoZBTwJ`P)d;Z-ybvjw?C@%p1%8)l-|dWU;S%F|Fdr?y{B(}%%9XWFaPhj%sij4 z8yAn1{$iu4ede{zB&Yue?y!q(n)Y&fpC_Jy*Wc5B;l@WI`d6*|SFL=1u8aN=Zm|8m z{5_-JGy1cB!!pS8dkVyyd;Zux`)={QeSQBmO?u=X{5idk|DWJmT>kwxl;Hk>reEdF z=;!pFejN8>`rchD@TR7FTpXgG(|P*ojDGJ&Mt7*`mm*poI-OsB8uYy8?|i@s} fR&)K_B)%HT^{f1rZjJuYXZ7laSJViy)8798-(6*y literal 0 HcmV?d00001 diff --git a/StreamDock/Transport/TransportDLL/libtransport_arm64.dylib b/StreamDock/Transport/TransportDLL/libtransport_arm64.dylib new file mode 100644 index 0000000000000000000000000000000000000000..ba94c39b1fdf81672af92d74a302f4500ca2293f GIT binary patch literal 210728 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
i91c{~&z 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 1: + dot_r = 3 + dot_spacing = 14 + total_width = total_pages * dot_spacing + start_x = (size[0] - total_width) // 2 + page_label_y = size[1] - 20 + for i in range(total_pages): + cx = start_x + i * dot_spacing + dot_spacing // 2 + if i == page_num - 1: + draw.ellipse([cx - dot_r, page_label_y - dot_r, cx + dot_r, page_label_y + dot_r], fill=(255, 255, 255)) + else: + draw.ellipse([cx - dot_r, page_label_y - dot_r, cx + dot_r, page_label_y + dot_r], fill=(80, 80, 80)) + return img \ No newline at end of file diff --git a/streamdock_ha.py b/streamdock_ha.py new file mode 100644 index 0000000..1b1f7b8 --- /dev/null +++ b/streamdock_ha.py @@ -0,0 +1,423 @@ +import asyncio +import io +import logging +import sys +import signal + +from homeassistant_api import AsyncWebsocketClient, State +from StreamDock.DeviceManager import DeviceManager +from StreamDock.ImageHelpers.PILHelper import to_native_key_format, to_native_touchscreen_format +from StreamDock.InputTypes import EventType, Direction, KnobId +from config import load_config +from key_renderer import render_key_image, render_background + +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s [%(levelname)s] %(name)s: %(message)s", + datefmt="%H:%M:%S", +) +log = logging.getLogger("streamdock-ha") + + +def _parse_button_ref(ref): + if not ref: + return None + ref = str(ref).lower().strip() + mapping = { + "button_7": 7, "key_7": 7, + "button_8": 8, "key_8": 8, + "button_9": 9, "key_9": 9, + } + return mapping.get(ref, int(ref) if ref.isdigit() else None) + + +def _parse_knob_ref(ref): + if not ref: + return None + ref = str(ref).lower().strip().replace("-", "_") + mapping = { + "knob_1": KnobId.KNOB_1, "knob1": KnobId.KNOB_1, + "knob_2": KnobId.KNOB_2, "knob2": KnobId.KNOB_2, + "knob_3": KnobId.KNOB_3, "knob3": KnobId.KNOB_3, + "knob_4": KnobId.KNOB_4, "knob4": KnobId.KNOB_4, + "left": KnobId.KNOB_1, "bottom_left": KnobId.KNOB_1, + "right": KnobId.KNOB_2, "bottom_right": KnobId.KNOB_2, + "top": KnobId.KNOB_3, + } + if ref in mapping: + return mapping[ref] + for knob in KnobId: + if knob.value == ref: + return knob + return None + + +class StreamDockHA: + def __init__(self, config_path=None): + self.cfg = load_config(config_path) + self.ha_url = self.cfg["homeassistant"]["url"] + self.ha_token = self.cfg["homeassistant"]["token"] + self.pages = self.cfg.get("pages", []) + self.active_page_idx = 0 + + nav = self.cfg.get("navigation", {}) + self.page_cycle_key = _parse_button_ref(nav.get("page_cycle", "button_8")) + self.nav_knob_left = _parse_knob_ref(nav.get("knob_left")) + self.nav_knob_right = _parse_knob_ref(nav.get("knob_right")) + self.nav_knob_press_left = _parse_knob_ref(nav.get("knob_press_left")) + self.nav_knob_press_right = _parse_knob_ref(nav.get("knob_press_right")) + + self.device = None + self.ha_client = None + self.entity_states = {} + self._all_key_configs = [] + self._all_knob_configs = [] + self._running = False + + def _build_all_configs(self): + self._all_key_configs = [] + self._all_knob_configs = [] + for page in self.pages: + page_keys = {} + raw_keys = page.get("keys", {}) + for key_str, cfg in raw_keys.items(): + key_id = int(key_str) if isinstance(key_str, str) else key_str + page_keys[key_id] = cfg + self._all_key_configs.append(page_keys) + + page_knobs = {} + raw_knobs = page.get("knobs", {}) + for knob_ref, knob_cfg in raw_knobs.items(): + knob_id = _parse_knob_ref(knob_ref) + if knob_id: + page_knobs[knob_id] = knob_cfg + self._all_knob_configs.append(page_knobs) + + @property + def active_keys(self): + if not self._all_key_configs: + return {} + return self._all_key_configs[self.active_page_idx] + + @property + def active_knobs(self): + if not self._all_knob_configs: + return {} + return self._all_knob_configs[self.active_page_idx] + + def _all_entities(self): + entities = set() + for page_keys in self._all_key_configs: + for cfg in page_keys.values(): + entity = cfg.get("entity") + if entity: + entities.add(entity) + for page_knobs in self._all_knob_configs: + for knob_cfg in page_knobs.values(): + for action_key in ("press", "rotate_left", "rotate_right"): + action = knob_cfg.get(action_key, {}) + if isinstance(action, dict): + entity = action.get("entity") + if entity: + entities.add(entity) + entity = knob_cfg.get("entity") + if entity: + entities.add(entity) + return entities + + async def _fetch_initial_states(self): + if not self.ha_client: + return + entities = self._all_entities() + if not entities: + return + try: + states = await self.ha_client.get_states() + for state in states: + if state.entity_id in entities: + self.entity_states[state.entity_id] = state.state + log.info(f"Fetched states for {len(entities)} entities") + except Exception as e: + log.error(f"Failed to fetch initial states: {e}") + + async def _render_page(self): + if not self.device: + return + await self._render_background() + for key_id, cfg in self.active_keys.items(): + await self._update_key_image(key_id) + self.device.refresh() + log.info(f"Page rendered: {self.pages[self.active_page_idx].get('name', '')}") + + async def _render_background(self): + if not self.device: + return + active_name = self.pages[self.active_page_idx].get("name", "") + total = len(self.pages) + bg = render_background( + self.pages, active_name, + page_num=self.active_page_idx + 1, + total_pages=total, + ) + bg_processed = to_native_touchscreen_format(self.device, bg) + buf = io.BytesIO() + bg_processed.save(buf, format="JPEG", quality=85) + self.device.transport.set_background_image_stream(buf.getvalue()) + + async def _update_key_image(self, key_id): + cfg = self.active_keys.get(key_id) + if not cfg: + return + entity = cfg.get("entity") + state = self.entity_states.get(entity, "unknown") if entity else "off" + try: + img = render_key_image(cfg, state, size=64) + img_processed = to_native_key_format(self.device, img) + buf = io.BytesIO() + img_processed.save(buf, format="JPEG", quality=90) + jpeg_data = buf.getvalue() + self.device.transport.set_key_image_stream(jpeg_data, key_id) + except Exception as e: + log.error(f"Failed to update key {key_id}: {e}") + + async def _cycle_page(self, direction=1): + if len(self.pages) <= 1: + return + old_idx = self.active_page_idx + self.active_page_idx = (self.active_page_idx + direction) % len(self.pages) + if self.active_page_idx == old_idx: + return + page_name = self.pages[self.active_page_idx].get("name", "") + log.info(f"Page: {page_name} ({self.active_page_idx + 1}/{len(self.pages)})") + self.device.clearAllIcon() + await self._render_page() + + async def _handle_event(self, device, event): + if event.event_type == EventType.KNOB_ROTATE: + await self._handle_knob_rotate(event) + return + + if event.event_type == EventType.KNOB_PRESS: + await self._handle_knob_press(event) + return + + if event.event_type != EventType.BUTTON: + return + if event.state != 1: + return + + key_id = event.key.value if event.key else None + if key_id is None: + return + + if key_id == self.page_cycle_key: + await self._cycle_page(1) + return + + cfg = self.active_keys.get(key_id) + if not cfg: + return + await self._call_service_from_cfg(cfg, f"Key {key_id}") + + async def _handle_knob_rotate(self, event): + knob_id = event.knob_id + if not knob_id: + return + + is_right = event.direction == Direction.RIGHT + + if is_right and knob_id == self.nav_knob_right: + await self._cycle_page(1) + return + if not is_right and knob_id == self.nav_knob_left: + await self._cycle_page(-1) + return + + knob_cfg = self.active_knobs.get(knob_id) + if not knob_cfg: + return + + action_key = "rotate_right" if is_right else "rotate_left" + action = knob_cfg.get(action_key) + if not action: + return + await self._call_service_from_cfg(action, f"Knob {knob_id.value} {action_key}") + + async def _handle_knob_press(self, event): + knob_id = event.knob_id + if not knob_id: + return + + if event.state != 1: + return + + if knob_id == self.nav_knob_press_left: + await self._cycle_page(-1) + return + if knob_id == self.nav_knob_press_right: + await self._cycle_page(1) + return + + knob_cfg = self.active_knobs.get(knob_id) + if not knob_cfg: + return + + press_action = knob_cfg.get("press") + if not press_action: + entity = knob_cfg.get("entity") + service = knob_cfg.get("service") + if entity and service: + await self._call_service(entity, service, f"Knob {knob_id.value} press") + return + await self._call_service_from_cfg(press_action, f"Knob {knob_id.value} press") + + async def _call_service_from_cfg(self, cfg, label=""): + if isinstance(cfg, dict): + entity = cfg.get("entity") + service = cfg.get("service") + else: + return + if not entity or not service: + return + await self._call_service(entity, service, label) + + async def _call_service(self, entity, service, label=""): + domain = entity.split(".")[0] + log.info(f"{label}: calling {domain}.{service} on {entity}") + try: + await self.ha_client.trigger_service(domain, service, entity_id=entity) + except Exception as e: + log.error(f"Failed to call service {domain}.{service}: {e}") + + async def _listen_state_changes(self): + if not self.ha_client: + return + log.info("Listening for HA state changes...") + try: + async with self.ha_client.listen_events("state_changed") as events: + async for event in events: + if not self._running: + break + try: + data = event.data if hasattr(event, "data") else {} + entity_id = data.get("entity_id", "") + new_state = data.get("new_state") + if not entity_id or not new_state: + continue + if isinstance(new_state, State): + state_val = new_state.state + elif isinstance(new_state, dict): + state_val = new_state.get("state", "") + else: + state_val = str(new_state) + old_val = self.entity_states.get(entity_id) + self.entity_states[entity_id] = state_val + if old_val != state_val: + log.info(f"State changed: {entity_id} = {state_val}") + await self._update_visible_keys(entity_id) + except Exception as e: + log.error(f"Error processing state change: {e}") + except Exception as e: + log.error(f"HA event listener error: {e}") + + async def _update_visible_keys(self, entity_id): + for key_id, cfg in self.active_keys.items(): + if cfg.get("entity") == entity_id: + await self._update_key_image(key_id) + self.device.refresh() + + async def _setup_device(self): + manager = DeviceManager() + devices = manager.enumerate() + if not devices: + log.error("No StreamDock device found") + return False + + self.device = devices[0] + log.info( + f"Found {type(self.device).__name__} at {self.device.path} (serial: {self.device.serial_number})" + ) + self.device.open() + self.device.init() + self.device.set_key_callback(self._make_sync_callback()) + return True + + def _make_sync_callback(self): + loop = asyncio.get_event_loop() + + def callback(device, event): + asyncio.run_coroutine_threadsafe(self._handle_event(device, event), loop) + + return callback + + async def run(self): + if not self.pages: + log.error("No pages configured in config.yaml") + return + + if not await self._setup_device(): + return + + self._build_all_configs() + + async with AsyncWebsocketClient(self.ha_url, self.ha_token) as client: + self.ha_client = client + log.info("Connected to Home Assistant") + self._running = True + + await self._fetch_initial_states() + await self._render_page() + + listen_task = asyncio.create_task(self._listen_state_changes()) + + nav_info = [] + if self.page_cycle_key: + nav_info.append(f"Key {self.page_cycle_key}=next page") + if self.nav_knob_left or self.nav_knob_right: + nav_info.append("knob rotation=prev/next page") + log.info(f"StreamDock-HA running ({len(self.pages)} pages). {'; '.join(nav_info)}. Ctrl+C to exit.") + try: + while self._running: + await asyncio.sleep(1) + except (KeyboardInterrupt, asyncio.CancelledError): + pass + finally: + self._running = False + listen_task.cancel() + try: + await listen_task + except asyncio.CancelledError: + pass + if self.device: + self.device.close() + log.info("Shutdown complete") + + def shutdown(self): + self._running = False + + +def main(): + config_path = None + for i, arg in enumerate(sys.argv): + if arg in ("-c", "--config") and i + 1 < len(sys.argv): + config_path = sys.argv[i + 1] + + app = StreamDockHA(config_path) + + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + + for sig in (signal.SIGINT, signal.SIGTERM): + loop.add_signal_handler(sig, app.shutdown) + + try: + loop.run_until_complete(app.run()) + except KeyboardInterrupt: + app.shutdown() + loop.run_until_complete(app.run()) + finally: + loop.close() + + +if __name__ == "__main__": + main() \ No newline at end of file