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())
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)
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()