def set_color(self, channel, mode, colors, non_volatile=False, **kwargs): """Set the lighting mode, when applicable, color. The table bellow summarizes the available channels, modes and their associated number of required colors. | Channel | Mode | Required colors | | -------- | --------- | --------------- | | led | off | 0 | | led | fixed | 1 | | led | flash | 1 | | led | breathing | 1 | | led | rainbow | 0 | The settings configured on the device are normally volatile, and are cleared whenever the graphics card is powered down (OS and UEFI power saving settings can affect when this happens). It is possible to store them in non-volatile controller memory by passing `non_volatile=True`. But as this memory has some unknown yet limited maximum number of write cycles, volatile settings are preferable, if the use case allows for them. """ check_unsafe('smbus', error=True, **kwargs) assert self._address != self._SENTINEL_ADDRESS, \ 'invalid address (probing may not have had access to SMbus)' colors = list(colors) try: mode = self.Mode[mode] except KeyError: raise ValueError(f'invalid mode: {mode!r}') from None if len(colors) < mode.required_colors: raise ValueError(f'{mode} mode requires {mode.required_colors} colors') if len(colors) > mode.required_colors: _LOGGER.debug('too many colors, dropping to %d', mode.required_colors) colors = colors[:mode.required_colors] if mode == self.Mode.OFF: self._smbus.write_byte_data(self._address, self._REG_MODE, self.Mode.FIXED.value) self._smbus.write_byte_data(self._address, self._REG_RED, 0x00) self._smbus.write_byte_data(self._address, self._REG_GREEN, 0x00) self._smbus.write_byte_data(self._address, self._REG_BLUE, 0x00) else: self._smbus.write_byte_data(self._address, self._REG_MODE, mode.value) for r, g, b in colors: self._smbus.write_byte_data(self._address, self._REG_RED, r) self._smbus.write_byte_data(self._address, self._REG_GREEN, g) self._smbus.write_byte_data(self._address, self._REG_BLUE, b) if non_volatile: self._smbus.write_byte_data(self._address, self._REG_APPLY, self._ASUS_GPU_APPLY_VAL)
def set_color(self, channel, mode, colors, non_volatile=False, **kwargs): """Set the RGB lighting mode and, when applicable, color. The table bellow summarizes the available channels, modes and their associated number of required colors. | Channel | Mode | Required colors | | -------- | --------- | --------------- | | led | off | 0 | | led | fixed | 1 | | led | breathing | 1 | | led | rainbow | 0 | The settings configured on the device are normally volatile, and are cleared whenever the graphics card is powered down (OS and UEFI power saving settings can affect when this happens). It is possible to store them in non-volatile controller memory by passing `non_volatile=True`. But as this memory has some unknown yet limited maximum number of write cycles, volatile settings are preferable, if the use case allows for them. """ check_unsafe('smbus', error=True, **kwargs) colors = list(colors) try: mode = self.Mode[mode] except KeyError: raise ValueError(f'invalid mode: {mode!r}') from None if len(colors) < mode.required_colors: raise ValueError( f'{mode} mode requires {mode.required_colors} colors') if len(colors) > mode.required_colors: _LOGGER.debug('too many colors, dropping to %d', mode.required_colors) colors = colors[:mode.required_colors] self._smbus.write_byte_data(self._address, self._REG_MODE, mode.value) for r, g, b in colors: self._smbus.write_byte_data(self._address, self._REG_RED, r) self._smbus.write_byte_data(self._address, self._REG_GREEN, g) self._smbus.write_byte_data(self._address, self._REG_BLUE, b) if non_volatile: # the following write always fails, but nonetheless induces persistence try: self._smbus.write_byte_data(self._address, self._REG_PERSIST, self._PERSIST) except OSError as err: _LOGGER.debug( 'expected OSError when writing to _REG_PERSIST: %s', err)
def set_speed_profile(self, channel, profile, temperature_sensor=1, **kwargs): """Set fan or fans to follow a speed duty profile. Valid channel values are 'fanN', where N >= 1 is the fan number, and 'fan', to simultaneously configure all fans. Unconfigured fan channels may default to 100% duty. Up to six (temperature, duty) pairs can be supplied in `profile`, with temperatures in Celsius and duty values in percentage. The last point should set the fan to 100% duty cycle, or be omitted; in the latter case the fan will be set to max out at 60°C. """ # send fan num, temp sensor, check to make sure it is actually enabled, and do not let the user send external sensor # 6 2-byte big endian temps (celsius * 100), then 6 2-byte big endian rpms # need to figure out how to find out what the max rpm is for the given fan if self._fan_count == 0: raise NotSupportedByDevice() profile = list(profile) criticalTemp = _CRITICAL_TEMPERATURE_HIGH if check_unsafe( 'high_tempature', **kwargs) else _CRITICAL_TEMPERATURE profile = _prepare_profile(profile, criticalTemp) # fan_type = kwargs['fan_type'] # need to make sure this is set temp_sensor = clamp(temperature_sensor, 1, self._temp_probs) sensors = self._data.load('temp_sensors_connected', default=[0] * self._temp_probs) if sensors[temp_sensor - 1] != 1: raise ValueError('the specified tempature sensor is not connected') buf = bytearray(26) buf[1] = temp_sensor - 1 # 0 # use temp sensor 1 for i, entry in enumerate(profile): temp = entry[0] * 100 rpm = entry[1] # convert both values to 2 byte big endian values buf[2 + i * 2] = temp.to_bytes(2, byteorder='big')[0] buf[3 + i * 2] = temp.to_bytes(2, byteorder='big')[1] buf[14 + i * 2] = rpm.to_bytes(2, byteorder='big')[0] buf[15 + i * 2] = rpm.to_bytes(2, byteorder='big')[1] fan_channels = self._get_hw_fan_channels(channel) fan_modes = self._data.load('fan_modes', default=[0] * self._fan_count) for fan in fan_channels: mode = fan_modes[fan] if mode == _FAN_MODE_DC or mode == _FAN_MODE_PWM: buf[0] = fan self._send_command(_CMD_SET_FAN_PROFILE, buf)
def get_status(self, verbose=False, **kwargs): """Get a status report. Returns a list of `(property, value, unit)` tuples. """ # only RGB lighting information can be fetched for now; as that isn't # super interesting, only enable it in verbose mode if not verbose: return [] if not check_unsafe('smbus', **kwargs): _LOGGER.warning("%s: nothing returned, requires unsafe feature 'smbus'", self.description) return [] mode = self.Mode(self._smbus.read_byte_data(self._address, self._REG_MODE)) status = [('Mode', mode, '')] if mode.required_colors > 0: r = self._smbus.read_byte_data(self._address, self._REG_RED) g = self._smbus.read_byte_data(self._address, self._REG_GREEN) b = self._smbus.read_byte_data(self._address, self._REG_BLUE) status.append(('Color', f'{r:02x}{g:02x}{b:02x}', '')) return status
def connect(self, **kwargs): """Connect to the device.""" if not check_unsafe('smbus', **kwargs): _LOGGER.warning("SMBus: disabled, requires unsafe feature 'smbus'") return self._smbus.open() return self
def get_status(self, **kwargs): """Get a status report. Returns a list of `(property, value, unit)` tuples. """ if not check_unsafe(*self._UNSAFE, **kwargs): _LOGGER.warning("%s: disabled, requires unsafe features '%s'", self.description, ','.join(self._UNSAFE)) return [] treg = self._read_temperature_register() # discard flags bits and interpret remaining bits as 2s complement treg = treg & 0x1fff if treg > 0x0fff: treg -= 0x2000 # should always be supported resolution, bits = (.25, 10) multiplier = treg >> (12 - bits) return [ ('Temperature', resolution * multiplier, '°C'), ]
def probe(cls, smbus, vendor=None, product=None, address=None, match=None, release=None, serial=None, **kwargs): ADDRESSES = [0x29, 0x2a, 0x60] ASUS_GPU_MAGIC_VALUE = 0x1589 if (vendor and vendor != ASUS) \ or (address and int(address, base=16) not in ADDRESSES) \ or smbus.parent_subsystem_vendor != ASUS \ or smbus.parent_vendor != NVIDIA \ or smbus.parent_driver != 'nvidia' \ or release or serial: # will never match: always None return supported = [ (NVIDIA_RTX_2080_TI_REV_A, ASUS_STRIX_RTX_2080_TI_OC, 'ASUS Strix RTX 2080 Ti OC'), ] for (dev_id, sub_dev_id, desc) in supported: if (product and product != sub_dev_id) \ or (match and match.lower() not in desc.lower()) \ or smbus.parent_subsystem_device != sub_dev_id \ or smbus.parent_device != dev_id \ or not smbus.description.startswith('NVIDIA i2c adapter 1 '): continue selected_address = None if check_unsafe('smbus', **kwargs): for address in ADDRESSES: val1 = 0 val2 = 0 smbus.open() try: val1 = smbus.read_byte_data(address, 0x20) val2 = smbus.read_byte_data(address, 0x21) except: pass smbus.close() if val1 << 8 | val2 == ASUS_GPU_MAGIC_VALUE: selected_address = address break else: selected_address = cls._SENTINEL_ADDRESS _LOGGER.debug('unsafe features not enabled, using sentinel address') if selected_address is not None: dev = cls(smbus, desc, vendor_id=ASUS, product_id=dev_id, address=selected_address) _LOGGER.debug('instanced driver for %s at address %02x', desc, selected_address) yield dev
def connect(self, **kwargs): """Connect to the device.""" if not check_unsafe('smbus', **kwargs): # do not raise yet: some driver APIs may not access the bus after # all, and allowing the device to pseudo-connect is convenient # given the current API structure; APIs that do access the bus # should check for the 'smbus' feature themselves and, if # necessary, raise UnsafeFeaturesNotEnabled(*requirements) # (see also: check_unsafe(..., error=True)) _LOGGER.debug("SMBus is disabled, missing unsafe feature 'smbus'") return self._smbus.open() return self
def probe(cls, smbus, vendor=None, product=None, address=None, match=None, release=None, serial=None, **kwargs): ASUS_GPU_MAGIC_VALUE = 0x1589 pre_probed = super().pre_probe(smbus, vendor, product, address, match, release, serial, **kwargs) for dev_id, sub_dev_id, desc in pre_probed: selected_address = None if check_unsafe('smbus', **kwargs): for address in cls._ADDRESSES: val1 = 0 val2 = 0 smbus.open() try: val1 = smbus.read_byte_data(address, 0x20) val2 = smbus.read_byte_data(address, 0x21) except: pass smbus.close() if val1 << 8 | val2 == ASUS_GPU_MAGIC_VALUE: selected_address = address break else: selected_address = cls._SENTINEL_ADDRESS _LOGGER.debug( 'unsafe features not enabled, using sentinel address') if selected_address is not None: dev = cls(smbus, desc, vendor_id=ASUS, product_id=dev_id, address=selected_address) _LOGGER.debug('instanced driver for %s at address %02x', desc, selected_address) yield dev
def get_status(self, verbose=False, **kwargs): """Get a status report. Returns a list of `(property, value, unit)` tuples. """ # only RGB lighting information can be fetched for now; as that isn't # super interesting, only enable it in verbose mode if not verbose: return [] if not check_unsafe('smbus', **kwargs): _LOGGER.warning( "%s: nothing to return, requires unsafe features " "'smbus'", self.description) return [] assert self._address != self._SENTINEL_ADDRESS, \ 'invalid address (probing may not have had access to SMbus)' mode = self._smbus.read_byte_data(self._address, self._REG_MODE) red = self._smbus.read_byte_data(self._address, self._REG_RED) green = self._smbus.read_byte_data(self._address, self._REG_GREEN) blue = self._smbus.read_byte_data(self._address, self._REG_BLUE) # emulate `OFF` both ways if red == green == blue == 0: mode = 0 mode = self.Mode(mode) status = [('Mode', mode, '')] if mode.required_colors > 0: status.append(('Color', f'{red:02x}{green:02x}{blue:02x}', '')) return status
def set_color(self, channel, mode, colors, **kwargs): """Set the color of each LED. In reality the device does not have the concept of different channels or modes, but this driver provides a few for convenience. Animations still require successive calls to this API. The 'led' channel can be used to address individual LEDs, and supports the 'super-fixed', 'fixed' and 'off' modes. In 'super-fixed' mode, each color in `colors` is applied to one individual LED, successively. LEDs for which no color has been specified default to off/solid black. This is closest to how the device works. In 'fixed' mode, all LEDs are set to the first color taken from `colors`. The `off` mode is equivalent to calling this function with 'fixed' and a single solid black color in `colors`. The `colors` argument should be an iterable of one or more `[red, blue, green]` triples, where each red/blue/green component is a value in the range 0–255. The table bellow summarizes the available channels, modes, and their associated maximum number of colors for each device family. | Channel | Mode | LEDs | Platinum | Pro XT | | -------- | ----------- | ------------ | -------- | ------ | | led | off | synchronized | 0 | 0 | | led | fixed | synchronized | 1 | 1 | | led | super-fixed | independent | 24 | 16 | Note: lighting control of Pro XT devices is experimental and requires the `pro_xt_lighting` constant to be supplied in the `unsafe` iterable. """ if 'Pro XT' in self.description: check_unsafe('pro_xt_lighting', error=True, **kwargs) channel, mode, colors = channel.lower(), mode.lower(), list(colors) self._check_color_args(channel, mode, colors) if mode == 'off': expanded = [] elif (channel, mode) == ('led', 'super-fixed'): expanded = colors[:self._led_count] elif (channel, mode) == ('led', 'fixed'): expanded = list(itertools.chain(*([color] * self._led_count for color in colors[:1]))) else: assert False, 'assumed unreacheable' if self._data.load('leds_enabled', of_type=int, default=0) == 0: # These hex strings are currently magic values that work but Im not quite sure why. d1 = bytes.fromhex("0101ffffffffffffffffffffffffff7f7f7f7fff00ffffffff00ffffffff00ffffffff00ffffffff00ffffffff00ffffffffffffffffffffffffffffff") d2 = bytes.fromhex("000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f2021222324252627ffffffffffffffffffffffffffffffffffffffffff") d3 = bytes.fromhex("28292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4fffffffffffffffffffffffffffffffffffffffffff") # Send the magic messages to enable setting the LEDs to statuC values self._send_command(None, 0b001, data=d1) self._send_command(None, 0b010, data=d2) self._send_command(None, 0b011, data=d3) self._data.store('leds_enabled', 1) data1 = bytes(itertools.chain(*((b, g, r) for r, g, b in expanded[0:20]))) data2 = bytes(itertools.chain(*((b, g, r) for r, g, b in expanded[20:]))) self._send_command(_FEATURE_LIGHTING, _CMD_SET_LIGHTING1, data=data1) self._send_command(_FEATURE_LIGHTING, _CMD_SET_LIGHTING2, data=data2)
def set_color(self, channel, mode, colors, speed='normal', transition_ticks=None, stable_ticks=None, **kwargs): """Set the RGB lighting mode and, when applicable, color. The table bellow summarizes the available channels, modes and their associated number of required colors. | Channel | Mode | Colors | | -------- | --------- | ------ | | led | off | 0 | | led | fixed | 1 | | led | breathing | 1–7 | | led | fading | 2–7 | The speed of the breathing and fading animations can be adjusted with `speed`; the allowed values are 'slowest', 'slower', 'normal' (default), 'faster' and 'fastest'. It is also possible to override the raw timing parameters through `transition_ticks` and `stable_ticks`; these should be integer values in the range 0–63. """ check_unsafe(*self._UNSAFE, error=True, **kwargs) try: common = self.SpeedTimings[speed].value tp1 = tp2 = common except KeyError: raise ValueError(f'invalid speed preset: {speed!r}') from None if transition_ticks is not None: tp1 = clamp(transition_ticks, 0, 63) if stable_ticks is not None: tp2 = clamp(stable_ticks, 0, 63) colors = list(colors) try: mode = self.Mode[mode] except KeyError: raise ValueError(f'invalid mode: {mode!r}') from None if len(colors) < mode.min_colors: raise ValueError(f'{mode} mode requires {mode.min_colors} colors') if len(colors) > mode.max_colors: _LOGGER.debug('too many colors, dropping to %d', mode.max_colors) colors = colors[:mode.max_colors] self._compute_rgb_address() if mode == self.Mode.OFF: mode = self.Mode.FIXED colors = [[0x00, 0x00, 0x00]] def rgb_write(register, value): self._smbus.write_byte_data(self._rgb_address, register, value) if mode == self.Mode.FIXED: rgb_write(self._REG_RGB_TIMING1, 0x00) else: rgb_write(self._REG_RGB_TIMING1, tp1) rgb_write(self._REG_RGB_TIMING2, tp2) color_registers = range(self._REG_RGB_COLOR_START, self._REG_RGB_COLOR_END) color_components = itertools.chain(*colors) for register, component in zip(color_registers, color_components): rgb_write(register, component) rgb_write(self._REG_RGB_COLOR_COUNT, len(colors)) if mode == self.Mode.BREATHING and len(colors) == 1: rgb_write(self._REG_RGB_MODE, self.Mode.FIXED.value) else: rgb_write(self._REG_RGB_MODE, mode.value)