Beispiel #1
0
class MacroDevice:

    def __init__(self, driver):

        if driver.input_manager is None:
            raise ValueError('This device does not support input events')

        self._driver = driver
        self._logger = driver.logger

        self._macro_keys = driver.hardware.macro_keys

        self._uinput = None
        self._queue = None

        self._task = None
        self._running = False


    async def _listen(self):
        while self._running:
            event = await self._queue.get_events()
            self._logger.info('event: %s', event)
            if event.scancode in self._macro_keys['numeric']: 
                self._logger.info('macro')


    def start(self):
        if self._running:
            return

        self._uinput = UInput.from_device(driver.input_manager.input_devices[0], \
            name='UChroma Virtual Macro Device %d' % driver.device_index)
        self._queue = InputQueue(driver)

        self._queue.attach()
        self._driver.input_manager.grab(True)

        self._task = ensure_future(self._listen())
        self._running = True


    def stop(self):
        if not self._running:
            return

        self._running = False

        self._task.cancel()

        self._driver.input_manager.grab(False)
        ensure_future(self._queue.detach())
Beispiel #2
0
class Renderer(HasTraits, object):
    """
    Base class for custom effects renderers.
    """

    # traits
    meta = RendererMeta('_unknown_', 'Unimplemented', 'Unknown', '0')

    fps = Float(min=0.0, max=MAX_FPS,
                default_value=DEFAULT_FPS).tag(config=True)
    blend_mode = DefaultCaselessStrEnum(BlendOp.get_modes(),
                                        default_value='screen',
                                        allow_none=False).tag(config=True)
    opacity = Float(min=0.0, max=1.0, default_value=1.0).tag(config=True)
    background_color = ColorTrait().tag(config=True)

    height = WriteOnceInt()
    width = WriteOnceInt()
    zindex = Int(default_value=-1)
    running = Bool(False)

    def __init__(self, driver, *args, **kwargs):
        self._avail_q = asyncio.Queue(maxsize=NUM_BUFFERS)
        self._active_q = asyncio.Queue(maxsize=NUM_BUFFERS)

        self.running = False

        self.width = driver.width
        self.height = driver.height

        self._tick = Ticker(1 / DEFAULT_FPS)

        self._input_queue = None
        if hasattr(driver,
                   'input_manager') and driver.input_manager is not None:
            self._input_queue = InputQueue(driver)

        self._logger = Log.get('uchroma.%s.%d' %
                               (self.__class__.__name__, self.zindex))
        super(Renderer, self).__init__(*args, **kwargs)

    @observe('zindex')
    def _z_changed(self, change):
        if change.old == change.new and change.new >= 0:
            return

        self._logger = Log.get('uchroma.%s.%d' %
                               (self.__class__.__name__, change.new))

    def init(self, frame) -> bool:
        """
        Invoked by AnimationLoop when the effect is activated. At this
        point, the traits will have been set. An implementation
        should perform any final setup here.

        :param frame: The frame instance being configured

        :return: True if the renderer was configured
        """
        return False

    def finish(self, frame):
        """
        Invoked by AnimationLoop when the effect is deactivated.
        An implementation should perform cleanup tasks here.

        :param frame: The frame instance being shut down
        """
        pass

    @abstractmethod
    async def draw(self, layer: Layer, timestamp: float) -> bool:
        """
        Coroutine called by AnimationLoop when a new frame needs
        to be drawn. If nothing should be drawn (such as if keyboard
        input is needed), then the implementation should yield until
        ready.

        :param layer: Layer to draw
        :param timestamp: The timestamp of this frame

        :return: True if the frame has been drawn
        """
        return False

    @property
    def has_key_input(self) -> bool:
        """
        True if the device is capable of producing key events
        """
        return self._input_queue is not None

    @property
    def key_expire_time(self) -> float:
        """
        Gets the duration (in seconds) that key events will remain
        available.
        """
        return self._input_queue.expire_time

    @key_expire_time.setter
    def key_expire_time(self, expire_time: float):
        """
        Set the duration (in seconds) that key events should remain
        in the queue for. This allows the renderer to act on groups
        of key events over time. If zero, events are not kept after
        being dequeued.
        """
        self._input_queue.expire_time = expire_time

    async def get_input_events(self):
        """
        Gets input events, yielding until at least one event is
        available. If expiration is not enabled, this returns
        a single item. Otherwise a list of all unexpired events
        is returned.
        """
        if not self.has_key_input or not self._input_queue.attach():
            raise ValueError('Input events are not supported for this device')

        events = await self._input_queue.get_events()
        return events

    @observe('fps')
    def _fps_changed(self, change):
        self._tick.interval = 1 / self.fps

    @property
    def logger(self):
        """
        The logger for this instance
        """
        return self._logger

    def _free_layer(self, layer):
        """
        Clear the layer and return it to the queue

        Called by AnimationLoop after a layer is replaced on the
        active list. Implementations should not call this directly.
        """
        layer.lock(False)
        layer.clear()
        self._avail_q.put_nowait(layer)

    async def _run(self):
        """
        Coroutine which dequeues buffers for drawing and queues them
        to the AnimationLoop when drawing is done.
        """
        if self.running:
            return

        self.running = True

        while self.running:
            async with self._tick:
                # get a buffer, blocking if necessary
                layer = await self._avail_q.get()
                layer.background_color = self.background_color
                layer.blend_mode = self.blend_mode
                layer.opacity = self.opacity

                try:
                    # draw the layer
                    status = await self.draw(layer,
                                             asyncio.get_event_loop().time())
                except Exception as err:
                    self.logger.exception(
                        "Exception in renderer, exiting now!", exc_info=err)
                    self.logger.error('Renderer traits: %s',
                                      self._trait_values)
                    break

                if not self.running:
                    break

                # submit for composition
                if status:
                    layer.lock(True)
                    await self._active_q.put(layer)

        await self._stop()

    def _flush(self):
        if self.running:
            return
        for qlen in range(0, self._avail_q.qsize()):
            self._avail_q.get_nowait()
        for qlen in range(0, self._active_q.qsize()):
            self._active_q.get_nowait()

    async def _stop(self):
        if not self.running:
            return

        self.running = False

        self._flush()

        if self.has_key_input:
            await self._input_queue.detach()

        self.logger.info("Renderer stopped: z=%d", self.zindex)
Beispiel #3
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()