예제 #1
0
파일: led.py 프로젝트: knownunown/uchroma
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])
예제 #2
0
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()
예제 #3
0
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()
예제 #4
0
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)
예제 #5
0
파일: device_base.py 프로젝트: sj26/uchroma
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)
예제 #6
0
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()
예제 #7
0
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()