def __init__(self, renderer: Renderer, frame: Frame, blend_mode=None, *args, **kwargs): super(LayerHolder, self).__init__(*args, **kwargs) self._renderer = renderer self._frame = frame self._blend_mode = blend_mode self.waiter = None self.active_buf = None self.task = None self.traits_changed = Signal() self._renderer.observe(self._traits_changed, names=['all']) self._renderer._flush() for buf in range(0, NUM_BUFFERS): layer = self._frame.create_layer() layer.blend_mode = self._blend_mode self._renderer._free_layer(layer)
def __init__(self, driver): self._driver = driver self._leds = {} self.led_changed = Signal() driver.restore_prefs.connect(self._restore_prefs)
def __init__(self, parent, layer, *args, **kwargs): self._delegate = layer self._zindex = layer.zindex super(LayerAPI, self).__init__(parent, *args, **kwargs) self.__class__.dbus = None self.layer_stopped = Signal() self._delegate.observe(self._z_changed, names=['zindex']) self._delegate.observe(self._state_changed, names=['running'])
class LEDManager: def __init__(self, driver): self._driver = driver self._leds = {} self.led_changed = Signal() driver.restore_prefs.connect(self._restore_prefs) @property def supported_leds(self): return self._driver.hardware.supported_leds def get(self, led_type: LEDType) -> LED: """ Fetches the requested LED interface on this device :param led_type: The LED type to fetch :return: The LED interface, if available """ if led_type not in self._driver.supported_leds: return None if led_type not in self._leds: self._leds[led_type] = LED(self._driver, led_type) self._leds[led_type].observe(self._led_changed) return self._leds[led_type] def _led_changed(self, change): self.led_changed.fire(change.owner) def _restore_prefs(self, prefs): led_prefs = prefs.leds for led_type in self.supported_leds: if led_type == LEDType.BACKLIGHT: # handled elsewhere continue key = led_type.name.lower() led = self.get(led_type) if led_prefs is not None and key in led_prefs: led.set_values(led_prefs[key])
def __init__(self, driver, bus): self._driver = driver self._bus = bus self._logger = driver.logger self.__class__.dbus = self._get_descriptor() self._signal_input = False self._input_task = None self._input_queue = None self._services = [] self._handle = None self.publish_changed = Signal() if self._logger.isEnabledFor(logging.DEBUG): self._logger.debug('Device API attached: %s', self.__class__.dbus)
def __init__(self, driver): super(AnimationManager, self).__init__() self._driver = driver self._loop = None self._logger = driver.logger self._error = False self.layers_changed = Signal() self.state_changed = Signal() driver.power_state_changed.connect(self._power_state_changed) driver.restore_prefs.connect(self._restore_prefs) self._renderer_info = self._discover_renderers() self._shutting_down = False
def __init__(self, hardware: Hardware, devinfo: hidapi.DeviceInfo, index: int, sys_path: str, input_devices=None, *args, **kwargs): self._hardware = hardware self._devinfo = devinfo self._devindex = index self._sys_path = sys_path self.logger = get_logger('uchroma.driver-%d' % index) # needed for mixins super(BaseUChromaDevice, self).__init__(*args, **kwargs) self._dev = None self._serial_number = None self._firmware_version = None self._last_cmd_time = None self._prefs = None self._offline = False self._suspended = False self.power_state_changed = Signal() self.restore_prefs = Signal() self._input_manager = None if input_devices is not None: self._input_manager = InputManager(self, input_devices) self._animation_manager = None if self.width > 0 and self.height > 0: self._animation_manager = AnimationManager(self) self._brightness_animator = ValueAnimator(self._update_brightness) self._fx_manager = None self._ref_count = 0 self._executor = futures.ThreadPoolExecutor(max_workers=1)
def __init__(self, frame: Frame, default_blend_mode: str = None, *args, **kwargs): super(AnimationLoop, self).__init__(*args, **kwargs) self._frame = frame self._default_blend_mode = default_blend_mode self._anim_task = None self._pause_event = asyncio.Event() self._pause_event.set() self._logger = frame._driver.logger self._error = False self.layers_changed = Signal()
def __init__(self, *callbacks): self._logger = Log.get('uchroma.devicemanager') self._devices = OrderedDict() self._monitor = False self._udev_context = Context() self._udev_observer = None self._callbacks = [] if callbacks is not None: self._callbacks.extend(callbacks) self._loop = asyncio.get_event_loop() self.device_added = Signal() self.device_removed = Signal() self.discover()
class AnimationLoop(HasTraits): layers = List(default_value=(), allow_none=False) running = Bool() """ Collects the output of one or more Renderers and displays the composited image. The loop is a fully asynchronous design, and renderers may independently block or yield buffers at different rates. Each renderer has a pair of asyncio.Queue objects and will put buffers onto the "active" queue when their draw cycle is completed. The loop yields on these queues until at least one buffer is available. All new buffers are placed on the "active" list and the previous buffers are returned to the respective renderer on the "avail" queue. If a renderer doesn't produce any output during the round, the current buffer is kept. The active list is finally composed and sent to the hardware. The design of this loop intends to be as CPU-efficient as possible and does not wake up spuriously or otherwise consume cycles while inactive. """ def __init__(self, frame: Frame, default_blend_mode: str = None, *args, **kwargs): super(AnimationLoop, self).__init__(*args, **kwargs) self._frame = frame self._default_blend_mode = default_blend_mode self._anim_task = None self._pause_event = asyncio.Event() self._pause_event.set() self._logger = frame._driver.logger self._error = False self.layers_changed = Signal() @observe('layers') def _start_stop(self, change): old = 0 if isinstance(change.old, list): old = len(change.old) new = len(change.new) if old == 0 and new > 0 and not self.running: self.start() elif new == 0 and old > 0 and self.running: self.stop() async def _dequeue(self, r_idx: int): """ Gather completed layers from the renderers. If nothing is available, keep the last layer (in case the renderers are producing output at different rates). Yields until at least one layer is ready. """ if not self.running or r_idx >= len(self.layers): return layer = self.layers[r_idx] renderer = layer.renderer # wait for a buffer buf = await renderer._active_q.get() # return the old buffer to the renderer if layer.active_buf is not None: renderer._free_layer(layer.active_buf) # put it on the active list layer.active_buf = buf def _dequeue_nowait(self, r_idx) -> bool: """ Variation of _dequeue which does not yield. :return: True if any layers became active """ if not self.running or r_idx >= len(self.layers): return False layer = self.layers[r_idx] renderer = layer.renderer # check if a buffer is ready if not renderer._active_q.empty(): buf = renderer._active_q.get_nowait() if buf is not None: # return the last buffer if layer.active_buf is not None: renderer._free_layer(layer.active_buf) # put it on the composition list layer.active_buf = buf return True return False async def _get_layers(self): """ Wait for renderers to produce new layers, yields until at least one layer is active. """ # schedule tasks to wait on each renderer queue for r_idx in range(0, len(self.layers)): layer = self.layers[r_idx] if layer.waiter is None or layer.waiter.done(): layer.waiter = ensure_future(self._dequeue(r_idx)) # async wait for at least one completion waiters = [layer.waiter for layer in self.layers] if len(waiters) == 0: return await asyncio.wait(waiters, return_when=futures.FIRST_COMPLETED) # check the rest without waiting for r_idx in range(0, len(self.layers)): layer = self.layers[r_idx] if layer.waiter is not None and not layer.waiter.done(): self._dequeue_nowait(r_idx) async def _commit_layers(self): """ Merge layers from all renderers and commit to the hardware """ if self._logger.isEnabledFor(LOG_TRACE - 1): self._logger.debug("Layers: %s", self.layers) active_bufs = [layer.active_buf for layer in \ sorted(self.layers, key=lambda z: z.zindex) \ if layer is not None and layer.active_buf is not None] try: if len(active_bufs) > 0: self._frame.commit(active_bufs) except (OSError, IOError): self._error = True await self._stop() async def _animate(self): """ Main loop Starts the renderers, waits for new layers to be drawn, composites the layers, sends them to the hardware, and finally syncs to achieve consistent frame rate. If no layers are ready, the loop yields to prevent spurious wakeups. """ self._logger.info("AnimationLoop is starting..") # start the renderers for layer in self.layers: layer.start() tick = Ticker(1 / MAX_FPS) # loop forever, waiting for layers while self.running: await self._pause_event.wait() async with tick: await self._get_layers() if not self.running: break # compose and display the frame await self._commit_layers() def _renderer_done(self, future): """ Invoked when the renderer exits """ self._logger.info("AnimationLoop is cleaning up") self._anim_task = None def _update_z(self, tmp_list): if len(tmp_list) > 0: for layer_idx in range(0, len(tmp_list)): tmp_list[layer_idx].renderer.zindex = layer_idx # fires trait observer self.layers = tmp_list def _layer_traits_changed(self, *args): self.layers_changed.fire('modify', *args) def add_layer(self, renderer: Renderer, zindex: int = None) -> bool: with self.hold_trait_notifications(): if zindex is None: zindex = len(self.layers) if not renderer.init(self._frame): self._logger.error('Renderer %s failed to initialize', renderer.name) return False layer = LayerHolder(renderer, self._frame, self._default_blend_mode) tmp = self.layers[:] tmp.insert(zindex, layer) self._update_z(tmp) layer.traits_changed.connect(self._layer_traits_changed) if self.running: layer.start() self._logger.info("Layer created, renderer=%s zindex=%d", layer.renderer, zindex) self.layers_changed.fire('add', zindex, layer.renderer, error=self._error) return True async def remove_layer(self, layer_like): with self.hold_trait_notifications(): if isinstance(layer_like, LayerHolder): zindex = self.layers.index(layer_like) elif isinstance(layer_like, int): zindex = layer_like else: raise TypeError('Layer should be a holder or an index') if zindex >= 0 and zindex < len(self.layers): layer = self.layers[zindex] layer_id = id(self.layers[zindex]) await layer.stop() tmp = self.layers[:] del tmp[zindex] self._update_z(tmp) self.layers_changed.fire('remove', zindex, layer_id, error=self._error) self._logger.info("Layer %d removed", zindex) async def clear_layers(self): if len(self.layers) == 0: return False for layer in self.layers[::-1]: await self.remove_layer(layer) return True def start(self) -> bool: """ Start the AnimationLoop Initializes the renderers, zeros the buffers, and starts the loop. Requires an active asyncio event loop. :return: True if the loop was started """ if self.running: self._logger.error("Animation loop already running") return False if len(self.layers) == 0: self._logger.error("No renderers were configured") return False self._error = False self.running = True self._anim_task = ensure_future(self._animate()) self._anim_task.add_done_callback(self._renderer_done) return True async def _stop(self): """ Stop this AnimationLoop Shuts down the loop and triggers cleanup tasks. """ if not self.running: return False self.running = False for layer in self.layers[::-1]: await self.remove_layer(layer) if self._anim_task is not None and not self._anim_task.done(): self._anim_task.cancel() await asyncio.wait([self._anim_task], return_when=futures.ALL_COMPLETED) self._logger.info("AnimationLoop stopped") def stop(self, cb=None): if not self.running: return False task = ensure_future(self._stop()) if cb is not None: task.add_done_callback(cb) return True def pause(self, paused): if paused != self._pause_event.is_set(): return self._logger.debug("Loop paused: %s", paused) if paused: self._pause_event.clear() else: self._pause_event.set()
class AnimationManager(HasTraits): """ Configures and manages animations of one or more renderers """ _renderer_info = FrozenDict() paused = Bool(False) def __init__(self, driver): super(AnimationManager, self).__init__() self._driver = driver self._loop = None self._logger = driver.logger self._error = False self.layers_changed = Signal() self.state_changed = Signal() driver.power_state_changed.connect(self._power_state_changed) driver.restore_prefs.connect(self._restore_prefs) self._renderer_info = self._discover_renderers() self._shutting_down = False @observe('paused') def _state_changed(self, change): # aggregate the trait notifications to a single signal value = 'stopped' if change.name == 'paused' and change.new and self.running: value = 'paused' elif change.name == 'running' and change.new and not self.paused: value = 'running' self.state_changed.fire(value) def _loop_running_changed(self, change): try: self._driver.reset() except (OSError, IOError): self._error = True self._state_changed(change) def _loop_layers_changed(self, *args, error=False): self.layers_changed.fire(*args) if not error: self._update_prefs() def _power_state_changed(self, brightness, suspended): if self.running and self.paused != suspended: self.pause(suspended) def _create_loop(self): if self._loop is None: self._loop = AnimationLoop(self._driver.frame_control) self._loop.observe(self._loop_running_changed, names=['running']) self._loop.layers_changed.connect(self._loop_layers_changed) def _update_prefs(self): if self._loop is None or self._shutting_down: return prefs = OrderedDict() for layer in self._loop.layers: prefs[layer.type_string] = layer.trait_values if len(prefs) > 0: self._driver.preferences.layers = prefs else: self._driver.preferences.layers = None def _discover_renderers(self): infos = OrderedDict() for ep_mod in iter_entry_points(group='uchroma.plugins', name='renderers'): obj = ep_mod.load() if not inspect.ismodule(obj): self._logger.error("Plugin %s is not a module, skipping", ep_mod) continue for ep_cls in iter_entry_points(group='uchroma.plugins', name='renderer'): obj = ep_cls.load() if not issubclass(obj, Renderer): self._logger.error("Plugin %s is not a renderer, skipping", ep_cls) continue for obj in Renderer.__subclasses__(): if inspect.isabstract(obj): continue if obj.meta.display_name == '_unknown_': self._logger.error( "Renderer %s did not set metadata, skipping", obj.__name__) continue key = '%s.%s' % (obj.__module__, obj.__name__) infos[key] = RendererInfo(obj.__module__, obj, key, obj.meta, obj.class_traits()) self._logger.debug("Loaded renderers: %s", ', '.join(infos.keys())) return infos def _get_renderer(self, name, zindex: int = None, **traits) -> Renderer: """ Instantiate a renderer :param name: Name of the discovered renderer :return: The renderer object """ info = self._renderer_info[name] try: return info.clazz(self._driver, **traits) except ImportError as err: self._logger.exception('Invalid renderer: %s', name, exc_info=err) return None def add_renderer(self, name, traits: dict, zindex: int = None) -> int: """ Adds a renderer which will produce a layer of this animation. Any number of renderers may be added and the output will be composited together. The z-order of the layers corresponds to the order renderers were added, with the first producing the base layer and the last producing the topmost layer. Renderers are defined in setup.py as entry points in group "uchroma.plugins". A module containing multiple renderers may be specified with "renderers" and a single class may be specified as "renderer". :param renderer: Key name of a discovered renderer :return: Z-position of the new renderer or -1 on error """ self._create_loop() if zindex is not None and zindex > len(self._loop.layers): raise ValueError("Z-index out of range (requested %d max %d)" % \ (zindex, len(self._loop.layers))) renderer = self._get_renderer(name, **traits) if renderer is None: self._logger.error('Renderer %s failed to load', renderer) return -1 if not self._loop.add_layer(renderer, zindex): self._logger.error('Renderer %s failed to initialize', name) return -1 return renderer.zindex def remove_renderer(self, zindex: int) -> bool: if self._loop is None: return False if zindex is None or zindex < 0 or zindex > len(self._loop.layers): self._logger.error("Z-index out of range (requested %d max %d)", zindex, len(self._loop.layers)) return False ensure_future(self._loop.remove_layer(zindex)) return True def pause(self, state=None): if self._loop is not None: if state is None: state = not self.paused if state != self.paused: self._loop.pause(state) self.paused = state self._logger.info("Animation paused: %s", self.paused) return self.paused def stop(self, cb=None): if self._loop is not None: return self._loop.stop(cb=cb) return False async def shutdown(self): """ Shuts down the animation service, waiting for all layers to finish work. This is a coroutine. """ self._shutting_down = True if self._loop is None: return await self._loop.clear_layers() def _restore_prefs(self, prefs): """ Restore active layers from preferences """ self._logger.debug('Restoring layers: %s', prefs.layers) if prefs.layers is not None and len(prefs.layers) > 0: try: for name, args in prefs.layers.items(): self.add_renderer(name, args) except Exception as err: self._logger.exception( 'Failed to add renderers, clearing! [%s]', prefs.layers, exc_info=err) self.stop() @property def renderer_info(self): """ The list of all discovered renderers """ return self._renderer_info @property def running(self): """ True if an animation is currently running """ return self._loop is not None and self._loop.running def __del__(self): if hasattr(self, '_loop') and self._loop is not None: self._loop.stop()
class LayerHolder(HasTraits): def __init__(self, renderer: Renderer, frame: Frame, blend_mode=None, *args, **kwargs): super(LayerHolder, self).__init__(*args, **kwargs) self._renderer = renderer self._frame = frame self._blend_mode = blend_mode self.waiter = None self.active_buf = None self.task = None self.traits_changed = Signal() self._renderer.observe(self._traits_changed, names=['all']) self._renderer._flush() for buf in range(0, NUM_BUFFERS): layer = self._frame.create_layer() layer.blend_mode = self._blend_mode self._renderer._free_layer(layer) @property def type_string(self): cls = self._renderer.__class__ return '%s.%s' % (cls.__module__, cls.__name__) @property def trait_values(self): return get_args_dict(self._renderer) def _traits_changed(self, change): if not self.renderer.running: return self.traits_changed.fire(self.zindex, self.trait_values, change.name, change.old) @property def zindex(self): return self._renderer.zindex @property def renderer(self): return self._renderer def start(self): if not self.renderer.running: self.task = ensure_future(self.renderer._run()) async def stop(self): if self.renderer.running: tasks = [] if self.task is not None and not self.task.done(): self.task.cancel() tasks.append(self.task) if self.waiter is not None and not self.waiter.done(): self.waiter.cancel() tasks.append(self.waiter) await self.renderer._stop() if len(tasks) > 0: await asyncio.wait(tasks, return_when=futures.ALL_COMPLETED) self.renderer.finish(self._frame)
class BaseUChromaDevice(object): """ Base class for device objects """ class Command(BaseCommand): """ Standard commands used by all Chroma devices """ # info queries, class 0 GET_FIRMWARE_VERSION = (0x00, 0x81, 0x02) GET_SERIAL = (0x00, 0x82, 0x16) def __init__(self, hardware: Hardware, devinfo: hidapi.DeviceInfo, index: int, sys_path: str, input_devices=None, *args, **kwargs): self._hardware = hardware self._devinfo = devinfo self._devindex = index self._sys_path = sys_path self.logger = get_logger('uchroma.driver-%d' % index) # needed for mixins super(BaseUChromaDevice, self).__init__(*args, **kwargs) self._dev = None self._serial_number = None self._firmware_version = None self._last_cmd_time = None self._prefs = None self._offline = False self._suspended = False self.power_state_changed = Signal() self.restore_prefs = Signal() self._input_manager = None if input_devices is not None: self._input_manager = InputManager(self, input_devices) self._animation_manager = None if self.width > 0 and self.height > 0: self._animation_manager = AnimationManager(self) self._brightness_animator = ValueAnimator(self._update_brightness) self._fx_manager = None self._ref_count = 0 self._executor = futures.ThreadPoolExecutor(max_workers=1) async def shutdown(self): """ Shuts down all services associated with the device and closes the HID instance. """ if asyncio.get_event_loop().is_running(): if hasattr(self, '_animation_manager' ) and self.animation_manager is not None: await self.animation_manager.shutdown() if hasattr(self, '_input_manager') and self._input_manager is not None: await self._input_manager.shutdown() self.close(True) def close(self, force: bool = False): if not force: if self.animation_manager is not None and self.is_animating: return if self._ref_count > 0: return if hasattr(self, '_dev') and self._dev is not None: try: self._dev.close() except Exception: pass self._dev = None def has_fx(self, fx_type: str) -> bool: """ Test if the device supports a particular built-in effect :param fx_type: the effect to test :return: true if the effect is supported """ if self.fx_manager is None: return False return fx_type in self.fx_manager.available_fx @property def animation_manager(self): """ Animation manager for this device """ if hasattr(self, '_animation_manager'): return self._animation_manager return None @property def is_animating(self): """ True if an animation is currently running """ if self.animation_manager is not None: return self.animation_manager.running return False @property def fx_manager(self): """ Built-in effects manager for this device """ return self._fx_manager @property def input_manager(self): """ Input manager service for this device """ return self._input_manager @property def input_devices(self): """ Input devices associated with this instance """ if self._input_manager is None: return None return self._input_manager.input_devices @property def hid(self): """ The lower-layer hidapi device """ return self._dev @property def last_cmd_time(self): """ Timestamp of the last command sent to the hardware, used for delay enforcement """ return self._last_cmd_time @last_cmd_time.setter def last_cmd_time(self, last_cmd_time): self._last_cmd_time = last_cmd_time def _set_brightness(self, level: float) -> bool: return False def _get_brightness(self) -> float: return 0.0 async def _update_brightness(self, level): await ensure_future(asyncio.get_event_loop().run_in_executor( \ self._executor, functools.partial(self._set_brightness, level))) suspended = self.suspended and level == 0 self.power_state_changed.fire(level, suspended) @property def suspended(self): """ The power state of the device, true if suspended """ return self._suspended def suspend(self, fast=False): """ Suspend the device Performs any actions necessary to suspend the device. By default, the current brightness level is saved and set to zero. """ if self._suspended: return self.preferences.brightness = self.brightness if fast: self._set_brightness(0) else: if self._device_open(): self._brightness_animator.animate(self.brightness, 0, done_cb=self._done_cb) self._suspended = True def resume(self): """ Resume the device Performs any actions necessary to resume the device. By default, the saved brightness level is restored. """ if not self._suspended: return self._suspended = False self.brightness = self.preferences.brightness @property def brightness(self): """ The current brightness level of the device lighting """ if self._suspended: return self.preferences.brightness return self._get_brightness() @brightness.setter def brightness(self, level: float): """ Set the brightness level of the main device lighting :param level: Brightness level, 0-100 """ if not self._suspended: if self._device_open(): self._brightness_animator.animate(self.brightness, level, done_cb=self._done_cb) self.preferences.brightness = level def _ensure_open(self) -> bool: try: if self._dev is None: self._dev = hidapi.Device(self._devinfo, blocking=False) except Exception as err: self.logger.exception("Failed to open connection", exc_info=err) return False return True def get_report(self, command_class: int, command_id: int, data_size: int, *args, transaction_id: None, remaining_packets: int = 0x00) -> RazerReport: """ Create and initialize a new RazerReport on this device """ if transaction_id is None: if self.has_quirk(Quirks.TRANSACTION_CODE_3F): transaction_id = 0x3F else: transaction_id = 0xFF report = RazerReport(self, command_class, command_id, data_size, transaction_id=transaction_id, remaining_packets=remaining_packets) if args is not None: for arg in args: if arg is not None: report.args.put(arg) return report def _get_timeout_cb(self): """ Getter for report timeout handler """ return None def run_with_result(self, command: BaseCommand, *args, transaction_id: int = 0xFF, delay: float = None, remaining_packets: int = 0x00) -> bytes: """ Run a command and return the result Executes the given command with the provided list of arguments, returning the result report. Transaction id is only necessary for specialized commands or hardware. The connection to the device will be automatically closed by default. :param command: The command to run :param args: The list of arguments to call the command with :type args: varies :param transaction_id: Transaction identified, defaults to 0xFF :return: The result report from the hardware """ report = self.get_report(*command.value, *args, transaction_id=transaction_id, remaining_packets=remaining_packets) result = None if self.run_report(report, delay=delay): result = report.result return result @synchronized def run_report(self, report: RazerReport, delay: float = None) -> bool: """ Runs a previously initialized RazerReport on the device :param report: the report to run :param delay: custom delay to enforce between commands :return: True if successful """ with self.device_open(): return report.run(delay=delay, timeout_cb=self._get_timeout_cb()) def run_command(self, command: BaseCommand, *args, transaction_id: int = 0xFF, delay: float = None, remaining_packets: int = 0x00) -> bool: """ Run a command Executes the given command with the provided list of arguments. Transaction id is only necessary for specialized commands or hardware. The connection to the device will be automatically closed by default. :param command: The command to run :param args: The list of arguments to call the command with :type args: varies :param transaction_id: Transaction identified, defaults to 0xFF :param timeout_cb: Callback to invoke on a timeout :return: True if the command was successful """ report = self.get_report(*command.value, *args, transaction_id=transaction_id, remaining_packets=remaining_packets) return self.run_report(report, delay=delay) def _decode_serial(self, value: bytes) -> str: if value is not None: try: return value.decode() except UnicodeDecodeError: return self.key return None def _get_serial_number(self) -> str: """ Get the serial number from the hardware directly Laptops don't return a serial number for their devices, so we return the model name. """ value = self.run_with_result(BaseUChromaDevice.Command.GET_SERIAL) return self._decode_serial(value) @property def serial_number(self) -> str: """ The hardware serial number of this device On laptops, this is not available. """ if self._serial_number is not None: return self._serial_number serial = self._get_serial_number() if serial is not None: self._serial_number = re.sub(r'\W+', r'', serial) return self._serial_number def _get_firmware_version(self) -> str: """ Get the firmware version from the hardware directly """ return self.run_with_result( BaseUChromaDevice.Command.GET_FIRMWARE_VERSION) @property def firmware_version(self) -> str: """ The firmware version present on this device """ if self._firmware_version is None: version = self._get_firmware_version() if version is None: self._firmware_version = '(unknown)' else: self._firmware_version = 'v%d.%d' % (int( version[0]), int(version[1])) return self._firmware_version @property def is_offline(self) -> bool: """ Some devices (such as wireless models) might be "offline" in that the dock or other USB receiver might be plugged in, but the actual device is switched off. In this case, we can't interact with it but it will still enumerate. """ return self._offline @property def name(self) -> str: """ The name of this device """ return self.hardware.name @property def device_index(self) -> int: """ The internal index of this device in the device manager """ return self._devindex @property def sys_path(self) -> str: """ The sysfs path of this device """ return self._sys_path @property def key(self) -> str: """ Unique key which identifies this device to the device manager """ return '%04x:%04x.%02d' % (self.vendor_id, self.product_id, self.device_index) @property def hardware(self) -> Hardware: """ The sub-enumeration of Hardware """ return self._hardware @property def product_id(self) -> int: """ The USB product identifier of this device """ return self._devinfo.product_id @property def vendor_id(self) -> int: """ The USB vendor identifier of this device """ return self._devinfo.vendor_id @property def manufacturer(self) -> str: """ The manufacturer of this device """ return self._hardware.manufacturer @property def device_type(self) -> Hardware.Type: """ The type of this device, from the Hardware.Type enumeration """ return self.hardware.type @property def driver_version(self): """ Get the uChroma version """ return __version__ @property def width(self) -> int: """ Gets the width of the key matrix (if applicable) """ if self.hardware.dimensions is None: return 0 return self.hardware.dimensions.x @property def height(self) -> int: """ Gets the height of the key matrix (if applicable) """ if self.hardware.dimensions is None: return 0 return self.hardware.dimensions.y @property def has_matrix(self) -> bool: """ True if the device supports matrix control """ return self.hardware.has_matrix def has_quirk(self, quirk) -> bool: """ True if the quirk is required for this device """ return self.hardware.has_quirk(quirk) @property def key_mapping(self): """ The mapping between keycodes and lighting matrix coordinates """ return self.hardware.key_mapping @property def preferences(self): """ Saved preferences for this device """ if self._prefs is None: self._prefs = PreferenceManager().get(self.serial_number) return self._prefs def reset(self) -> bool: """ Reset effects and other configuration to defaults """ return True def fire_restore_prefs(self): """ Restore saved preferences """ with self.preferences.observers_paused(): if hasattr( self, 'brightness') and self.preferences.brightness is not None: setattr(self, 'brightness', self.preferences.brightness) self.restore_prefs.fire(self.preferences) def __repr__(self): return "%s(name=%s, type=%s, product_id=0x%04x, index=%d)" % \ (self.__class__.__name__, self.name, self.device_type.value, self.product_id, self.device_index) def _device_open(self): self._ref_count += 1 return self._ensure_open() def _device_close(self): self._ref_count -= 1 self.close() def _done_cb(self, future): self._device_close() @contextmanager def device_open(self): try: if self._device_open(): yield finally: self._device_close() def __del__(self): self.close(force=True)
class DeviceAPI(object): """ D-Bus API for device properties and common hardware features. """ if dev_mode_enabled(): InputEvent = signal() _PROPERTIES = { 'battery_level': 'd', 'bus_path': 'o', 'device_index': 'u', 'device_type': 's', 'driver_version': 's', 'firmware_version': 's', 'has_matrix': 'b', 'height': 'i', 'is_charging': 'b', 'is_wireless': 'b', 'key': 's', 'manufacturer': 's', 'name': 's', 'product_id': 'u', 'revision': 'u', 'serial_number': 's', 'key_mapping': 'a{saau}', 'sys_path': 's', 'vendor_id': 'u', 'width': 'i', 'zones': 'as' } _RW_PROPERTIES = { 'polling_rate': 's', 'dpi_xy': 'ai', 'dock_brightness': 'd', 'dock_charge_color': 's' } def __init__(self, driver, bus): self._driver = driver self._bus = bus self._logger = driver.logger self.__class__.dbus = self._get_descriptor() self._signal_input = False self._input_task = None self._input_queue = None self._services = [] self._handle = None self.publish_changed = Signal() if self._logger.isEnabledFor(logging.DEBUG): self._logger.debug('Device API attached: %s', self.__class__.dbus) def __getattribute__(self, name): # Intercept everything and delegate to the device class by converting # names between the D-Bus conventions to Python conventions. prop_name = camel_to_snake(name) if (prop_name in DeviceAPI._PROPERTIES or prop_name in DeviceAPI._RW_PROPERTIES) \ and hasattr(self._driver, prop_name): value = getattr(self._driver, prop_name) if isinstance(value, Enum): return value.name.lower() if isinstance(value, Color): return value.html if isinstance(value, (list, tuple)) and len(value) > 0 and isinstance( value[0], Enum): return [x.name.lower() for x in value] return value else: return super(DeviceAPI, self).__getattribute__(name) def __setattr__(self, name, value): prop_name = camel_to_snake(name) if prop_name != name and prop_name in DeviceAPI._RW_PROPERTIES: setattr(self._driver, prop_name, value) else: super(DeviceAPI, self).__setattr__(name, value) @property def bus_path(self): """ Get the bus path for all services related to this device. """ return '/org/chemlab/UChroma/%s/%04x_%04x_%02d' % \ (self._driver.device_type.value, self._driver.vendor_id, self._driver.product_id, self._driver.device_index) @property def bus(self): return self._bus @property def driver(self): return self._driver def publish(self): if self._handle is not None: return self._handle = self._bus.register_object(self.bus_path, self, None) if hasattr(self.driver, 'fx_manager') and self.driver.fx_manager is not None: self._services.append(FXManagerAPI(self)) if hasattr(self.driver, 'animation_manager' ) and self.driver.animation_manager is not None: self._services.append(AnimationManagerAPI(self)) if hasattr(self.driver, 'led_manager') and self.driver.led_manager is not None: self._services.append(LEDManagerAPI(self)) self.publish_changed.fire(True) def unpublish(self): if self._handle is None: return self.publish_changed.fire(False) self._handle.unregister() self._handle = None async def _dev_mode_input(self): while self._signal_input: event = await self._input_queue.get_events() if event is not None: self.InputEvent(dbus_prepare(event)[0]) @property def SupportedLeds(self) -> list: return [x.name.lower() for x in self._driver.supported_leds] @property def InputSignalEnabled(self) -> bool: """ Enabling input signalling will fire D-Bus signals when keyboard input is received. This is used by the tooling and bringup utilities. Developer mode must be enabled in order for this to be available on the bus due to potential security issues. """ return self._signal_input @InputSignalEnabled.setter def InputSignalEnabled(self, state): if dev_mode_enabled(): if state == self._signal_input: return if state: if self._input_queue is None: self._input_queue = InputQueue(self._driver) self._input_queue.attach() self._input_task = ensure_future(self._dev_mode_input()) else: ensure_future(self._input_queue.detach()) self._input_task.cancel() self._input_queue = None self._signal_input = state @property def FrameDebugOpts(self) -> dict: return dbus_prepare(self._driver.frame_control.debug_opts, variant=True)[0] PropertiesChanged = signal() @property def Brightness(self): """ The current brightness level """ return self._driver.brightness @Brightness.setter def Brightness(self, value): """ Set the brightness level of the device lighting. 0.0 - 100.0 """ if value < 0.0 or value > 100.0: return old = self._driver.brightness self._driver.brightness = value if old != self._driver.brightness: self.PropertiesChanged('org.chemlab.UChroma.Device', {'Brightness': value}, []) @property def Suspended(self): """ True if the device is suspended """ return self._driver.suspended @Suspended.setter def Suspended(self, value): """ Set the suspended state of the device """ current = self._driver.suspended if value == current: return if value: self._driver.suspend() else: self._driver.resume() if current != self._driver.suspended: self.PropertiesChanged('org.chemlab.UChroma.Device', {'Suspended': self.Suspended}, []) def Reset(self): self._driver.reset() def _get_descriptor(self): builder = DescriptorBuilder(self, 'org.chemlab.UChroma.Device') for name, sig in DeviceAPI._PROPERTIES.items(): if hasattr(self._driver, name): builder.add_property(name, sig, False) for name, sig in DeviceAPI._RW_PROPERTIES.items(): if hasattr(self._driver, name): builder.add_property(name, sig, True) if hasattr(self._driver, 'brightness'): builder.add_property('brightness', 'd', True) if hasattr(self._driver, 'suspend') and hasattr( self._driver, 'resume'): builder.add_property('suspended', 'b', True) builder.add_method('reset') # tooling support, requires dev mode enabled if dev_mode_enabled() and self._driver.input_manager is not None: builder.add_property('frame_debug_opts', 'a{sv}', False) builder.add_property('input_signal_enabled', 'b', True) builder.add_signal('input_event', ArgSpec('out', 'event', 'a{sv}')) return builder.build()
class LayerAPI(TraitsPropertiesMixin, ManagedService): def __init__(self, parent, layer, *args, **kwargs): self._delegate = layer self._zindex = layer.zindex super(LayerAPI, self).__init__(parent, *args, **kwargs) self.__class__.dbus = None self.layer_stopped = Signal() self._delegate.observe(self._z_changed, names=['zindex']) self._delegate.observe(self._state_changed, names=['running']) def _z_changed(self, change): if change.old != change.new: self.publish() def _state_changed(self, change): if change.old != change.new: if change.new: self.publish() elif self._handle != None: self._logger.info("Layer stopped zindex=%d (%s)", self._zindex, self._delegate.meta) self.unpublish() self.layer_stopped.fire(self) @staticmethod def get_layer_path(path, zindex): return '%s/layer/%d' % (path, zindex) @property def layer_path(self): return LayerAPI.get_layer_path(self._path, self._zindex) @property def layer_type(self): return '%s.%s' % (self._delegate.__class__.__module__, self._delegate.__class__.__name__) @property def layer_zindex(self): return self._zindex def publish(self): if not self._delegate.running: return self.unpublish() self.__class__.dbus = self._get_descriptor() self._zindex = self._delegate.zindex self._handle = self._bus.register_object(self.layer_path, self, None) self._logger.info("Registered layer API: %s", self.layer_path) self._logger.debug('%s', self.__class__.dbus) def unpublish(self): if super().unpublish(): self._logger.info("Unregisted layer API: %s", self.layer_path) def _get_descriptor(self): exclude = ('blend_mode', 'opacity') if self._delegate.zindex > 0: exclude = ('background_color', ) builder = DescriptorBuilder(self._delegate, 'org.chemlab.UChroma.Layer', exclude) for k, v in self._delegate.meta._asdict().items(): attrib = snake_to_camel(k) setattr(self, attrib, v) builder.add_property(attrib, 's') setattr( self, 'Key', '%s.%s' % (self._delegate.__module__, self._delegate.__class__.__name__)) builder.add_property('Key', 's') return builder.build()