def set_speed_profile(self, channel, profile, **kwargs): """Set channel to use a speed profile.""" if channel != 'pump': assert False, 'kraken X3 devices only support changing pump speeds' header = [0x72, 0x01, 0x00, 0x00] norm = normalize_profile(profile, _CRITICAL_TEMPERATURE) interp = [(interpolate_profile(norm, t)) for t in range(20, 60)] LOGGER.debug('setting pump curve: %s', [(num + 20, duty) for (num, duty) in enumerate(interp)]) self._write(header + interp)
def set_speed_profile(self, channel, profile, **kwargs): """Set channel to use a speed profile.""" cid, dmin, dmax = self._speed_channels[channel] header = [0x72, cid, 0x00, 0x00] norm = normalize_profile(profile, _CRITICAL_TEMPERATURE) stdtemps = list(range(20, _CRITICAL_TEMPERATURE + 1)) interp = [clamp(interpolate_profile(norm, t), dmin, dmax) for t in stdtemps] for temp, duty in zip(stdtemps, interp): _LOGGER.info('setting %s PWM duty to %d%% for liquid temperature >= %d°C', channel, duty, temp) self._write(header + interp)
def set_speed_profile(self, channel, profile, **kwargs): """Set channel to use a speed profile.""" if not self.supports_cooling_profiles: raise NotSupportedByDevice() norm = normalize_profile(profile, _CRITICAL_TEMPERATURE) # due to a firmware limitation the same set of temperatures must be # used on both channels; we reduce the number of writes by trimming the # interval and/or resolution to the most useful range stdtemps = list(range(20, 50)) + list(range(50, 60, 2)) + [60] interp = [(t, interpolate_profile(norm, t)) for t in stdtemps] cbase, dmin, dmax = _SPEED_CHANNELS[channel] for i, (temp, duty) in enumerate(interp): duty = clamp(duty, dmin, dmax) LOGGER.info('setting %s PWM duty to %i%% for liquid temperature >= %i°C', channel, duty, temp) self._write([0x2, 0x4d, cbase + i, temp, duty])
def set_speed_profile(self, channel, profile, **kwargs): """Set channel to use a speed profile.""" if not self.supports_cooling_profiles: raise NotImplementedError() cbase, dmin, dmax = _SPEED_CHANNELS[channel] # ideally we could just call normalize_profile (optionally followed by autofill_profile), # but Kraken devices currently require the same set of temperatures on both channels stdtemps = range(20, 62, 2) tmp = normalize_profile(profile, _CRITICAL_TEMPERATURE) norm = [(t, interpolate_profile(tmp, t)) for t in stdtemps] for i, (temp, duty) in enumerate(norm): duty = clamp(duty, dmin, dmax) LOGGER.info('setting %s PWM duty to %i%% for liquid temperature >= %i°C', channel, duty, temp) self._write([0x2, 0x4d, cbase + i, temp, duty]) self.device.release()
def control(device, channels, profiles, sensors, update_interval): LOGGER.info('device: %s on bus %s and address %s', device.description, device.bus, device.address) for channel, profile, sensor in zip(channels, profiles, sensors): LOGGER.info('channel: %s following profile %s on %s', channel, str(profile), sensor) averages = [None] * len(channels) cutoff_freq = 1 / update_interval / 10 alpha = 1 - math.exp(-2 * math.pi * cutoff_freq) LOGGER.info('update interval: %d s; cutoff frequency (low-pass): %.2f Hz; ema alpha: %.2f', update_interval, cutoff_freq, alpha) try: # more efficient and safer API, but only supported by very few devices apply_duty = device.set_instantaneous_speed except AttributeError: apply_duty = device.set_fixed_speed LOGGER.info('starting...') failures = 0 while True: try: sensor_data = read_sensors(device) for i, (channel, profile, sensor) in enumerate(zip(channels, profiles, sensors)): # compute the exponential moving average (ema), used as a low-pass filter (lpf) ema = averages[i] sample = sensor_data[sensor] if ema is None: ema = sample else: ema = alpha * sample + (1 - alpha) * ema averages[i] = ema # interpolate on sensor ema and apply corresponding duty duty = interpolate_profile(profile, ema) LOGGER.info('%s control: lpf(%s) = lpf(%.1f°C) = %.1f°C => duty := %d%%', channel, sensor, sample, ema, duty) apply_duty(channel, duty) failures = 0 except Exception as err: failures += 1 LOGGER.error(err) if failures >= MAX_FAILURES: LOGGER.critical('too many failures in a row: %d', failures) raise time.sleep(update_interval)