diff --git a/feeph/emc2101/__init__.py b/feeph/emc2101/__init__.py index e7f08f1..f9b3132 100755 --- a/feeph/emc2101/__init__.py +++ b/feeph/emc2101/__init__.py @@ -47,6 +47,8 @@ # the following imports are provided for user convenience # flake8: noqa: F401 from feeph.emc2101.calibration import calibrate_pwm_fan +from feeph.emc2101.config_register import ConfigRegister from feeph.emc2101.core import CONVERSIONS_PER_SECOND, DEFAULTS, ExternalSensorStatus, SpinUpDuration, SpinUpStrength +from feeph.emc2101.ets_config import ExternalTemperatureSensorConfig, ets_2n3904, ets_2n3906 from feeph.emc2101.fan_configs import FanConfig, RpmControlMode, Steps, export_fan_config, generic_pwm_fan -from feeph.emc2101.pwm import DeviceConfig, Emc2101_PWM, ExternalTemperatureSensorConfig, FanSpeedUnit, PinSixMode, TemperatureLimitType, emc2101_default_config, ets_2n3904, ets_2n3906 +from feeph.emc2101.pwm import DeviceConfig, Emc2101_PWM, FanSpeedUnit, PinSixMode, emc2101_default_config diff --git a/feeph/emc2101/calibration.py b/feeph/emc2101/calibration.py index e6fc981..bc8bdd7 100755 --- a/feeph/emc2101/calibration.py +++ b/feeph/emc2101/calibration.py @@ -27,8 +27,9 @@ def calibrate_pwm_fan(i2c_bus: busio.I2C, model: str, pwm_frequency: int = 22500 LH.info("Calibrating fan parameters.") pwm_d, pwm_f = feeph.emc2101.utilities.calculate_pwm_factors(pwm_frequency=pwm_frequency) steps_list = list(range(pwm_f * 2)) - emc2101 = feeph.emc2101.core.Emc2101_core(i2c_bus=i2c_bus) - emc2101.configure_pin_six_as_tacho() + # tacho signal on pin 6, device uses PWM control + config = feeph.emc2101.core.ConfigRegister(alt_tach=True, dac=False) + emc2101 = feeph.emc2101.core.Emc2101(i2c_bus=i2c_bus, config=config) emc2101.configure_pwm_control(pwm_d=pwm_d, pwm_f=pwm_f, step_max=max(steps_list)) # ----------------------------------------------------------------- LH.debug("Disabling gradual speed rampup.") diff --git a/feeph/emc2101/config_register.py b/feeph/emc2101/config_register.py new file mode 100755 index 0000000..c40f9d0 --- /dev/null +++ b/feeph/emc2101/config_register.py @@ -0,0 +1,72 @@ +#!/usr/bin/env + +from attrs import define + + +@define(eq=True) +class ConfigRegister: + """ + a representation of the EMC2101's config register (0x03) + + this is not the entire configuration, there are additional registers + which configure different aspects of this chip, e.g. fan configuration + register (0x4A) + + for an exhaustive description refer to EMC2101 datasheet section 6.5 + """ + # the comment describes what happens if the value is set to True + mask: bool = False # disable ALERT/TACH when in interrupt mode + standby: bool = False # enable low power standby mode + fan_standby: bool = False # disable fan output while in standby + dac: bool = False # enable DAC output on FAN pin + dis_to: bool = False # disable SMBUS timeout + alt_tach: bool = False # configure pin six as tacho input + trcit_ovrd: bool = False # unlock tcrit limit and allow one-time write + queue: bool = False # alert after 3 critical temperature readings + + def as_int(self): + """ + compute the config register's value + """ + config = 0x00 + if self.mask: + config |= 0b1000_0000 + if self.standby: + config |= 0b0100_0000 + if self.fan_standby: + config |= 0b0010_0000 + if self.dac: + config |= 0b0001_0000 + if self.dis_to: + config |= 0b0000_1000 + if self.alt_tach: + config |= 0b0000_0100 + if self.trcit_ovrd: + config |= 0b0000_0010 + if self.queue: + config |= 0b0000_0001 + return config + + +def parse_config_register(value: int) -> ConfigRegister: + """ + parse the config register's value + """ + params = dict() + if value & 0b1000_0000: + params['mask'] = True + if value & 0b0100_0000: + params['standby'] = True + if value & 0b0010_0000: + params['fan_standby'] = True + if value & 0b0001_0000: + params['dac'] = True + if value & 0b0000_1000: + params['dis_to'] = True + if value & 0b0000_0100: + params['alt_tach'] = True + if value & 0b0000_0010: + params['trcit_ovrd'] = True + if value & 0b0000_0001: + params['queue'] = True + return ConfigRegister(**params) diff --git a/feeph/emc2101/core.py b/feeph/emc2101/core.py index 77ea3b3..180ac96 100755 --- a/feeph/emc2101/core.py +++ b/feeph/emc2101/core.py @@ -13,11 +13,13 @@ # module busio provides no type hints import busio # type: ignore -from feeph.i2c import BurstHandler +from feeph.i2c import BurstHandle, BurstHandler +from feeph.emc2101.config_register import ConfigRegister, parse_config_register from feeph.emc2101.conversions import convert_bytes2temperature, convert_temperature2bytes +from feeph.emc2101.ets_config import ExternalTemperatureSensorConfig -LH = logging.getLogger(__name__) +LH = logging.getLogger('feeph.emc2101') DEFAULTS = { @@ -104,29 +106,27 @@ class SpinUpDuration(Enum): TIME_3_20 = 0b0000_0111 # 3.20s (default) -class Emc2101_core: +class Emc2101: """ low-level interface to the EMC2101 chip You probably don't want to use this one. Use Emc2101_DAC / Emc2101_PWM instead. """ - def __init__(self, i2c_bus: busio.I2C): + def __init__(self, i2c_bus: busio.I2C, config: ConfigRegister): """ initialize the object - Configure pin 6 and the control mode before use. - These settings MUST match the electric circuit! - - emc2101.configure_pin_six_as_alert() - - emc2101.configure_pin_six_as_tacho() - - emc2101.configure_dac_control() - - emc2101.configure_pwm_control() + Make sure to configure pin 6 and the control mode before use: + - config.alt_tach + - config.dac - If you don't set these values correctly you won't get sensible - readings! + These settings MUST match the electric circuit!If you don't set + these values correctly you won't get sensible readings! """ self._i2c_bus = i2c_bus self._i2c_adr = 0x4c # the I²C bus address is hardcoded + self.set_config_register(config) # allowed steps can be lower if PWM is used self._step_min = 0 self._step_max = 63 @@ -168,51 +168,28 @@ def describe_device(self): # fan speed control # --------------------------------------------------------------------- - def configure_pin_six_as_alert(self) -> bool: - try: - # set 0x03.2 to 0 - with BurstHandler(i2c_bus=self._i2c_bus, i2c_adr=self._i2c_adr) as bh: - cfg_register_value = bh.read_register(0x03) - bh.write_register(0x03, cfg_register_value & 0b1111_1011) - # clear spin up behavior settings - # (spin up is unavailable when pin 6 is in alert mode), - bh.write_register(0x4B, 0b0000_0000) - return True - except RuntimeError: - LH.error("Unable to read config register!") - return False - - def configure_pin_six_as_tacho(self) -> bool: - try: - # set 0x03.2 to 1 - with BurstHandler(i2c_bus=self._i2c_bus, i2c_adr=self._i2c_adr) as bh: - cfg_register_value = bh.read_register(0x03) - bh.write_register(0x03, cfg_register_value | 0b0000_0100) - return True - except RuntimeError: - LH.error("Unable to read config register!") - return False + def get_config_register(self) -> ConfigRegister: + with BurstHandler(i2c_bus=self._i2c_bus, i2c_adr=self._i2c_adr) as bh: + return _get_config_register(bh) - def configure_dac_control(self, step_max: int): - # enable DAC control (set 0x03.4 to 1) + def set_config_register(self, config: ConfigRegister): with BurstHandler(i2c_bus=self._i2c_bus, i2c_adr=self._i2c_adr) as bh: - cfg_register_value = bh.read_register(0x03) - bh.write_register(0x03, cfg_register_value | 0b0001_0000) - # configure maximum allowed step - self._step_max = step_max + _set_config_register(bh, config) - def configure_pwm_control(self, pwm_d: int, pwm_f: int, step_max: int): - # enable PWM control (set 0x03.4 to 0) + def configure_pwm_control(self, pwm_d: int, pwm_f: int, step_max: int) -> bool: with BurstHandler(i2c_bus=self._i2c_bus, i2c_adr=self._i2c_adr) as bh: - cfg_register_value = bh.read_register(0x03) - bh.write_register(0x03, cfg_register_value & 0b1110_1111) + # enable PWM control + if _get_config_register(bh).dac: + LH.warning("Unable to use PWM! Device is configured for direct current control.") + return False # configure pwm frequency divider settings bh.write_register(0x4D, pwm_f) bh.write_register(0x4E, pwm_d) # configure maximum allowed step self._step_max = step_max + return True - def configure_spinup_behaviour(self, spinup_strength: SpinUpStrength, spinup_duration: SpinUpDuration, fast_mode: bool): + def configure_spinup_behaviour(self, spinup_strength: SpinUpStrength, spinup_duration: SpinUpDuration, fast_mode: bool) -> bool: """ configure the spin-up behavior for the attached fan (duration and strength). This helps to ensure the fan has sufficient power @@ -226,15 +203,23 @@ def configure_spinup_behaviour(self, spinup_strength: SpinUpStrength, spinup_dur Please note: Fast_mode is ignored if pin 6 is in alert mode. """ - value = 0x00 - # configure spin up time - value |= spinup_duration.value - # configure spin up strength (dutycycle) - value |= spinup_strength.value - if fast_mode: - value |= 0b0010_0000 with BurstHandler(i2c_bus=self._i2c_bus, i2c_adr=self._i2c_adr) as bh: - bh.write_register(0x4B, value) + config = _get_config_register(bh) + if config.alt_tach: + # pin 6 is configured as tacho pin + value = 0x00 + # configure spin up time + value |= spinup_duration.value + # configure spin up strength (dutycycle) + value |= spinup_strength.value + if fast_mode: + value |= 0b0010_0000 + bh.write_register(0x4B, value) + return True + else: + # pin 6 is configured as alert pin + LH.warning("Pin 6 is in alert mode. Can't configure spinup behavior.") + return False def configure_minimum_rpm(self, minimum_rpm: int): """ @@ -375,6 +360,12 @@ def get_temperature_conversion_rate(self) -> str: value = min(value, 0b1001) # all values larger than 0b1001 map to 0b1001 return [k for k, v in CONVERSIONS_PER_SECOND.items() if v == value][0] + def get_temperature_conversion_rates(self) -> list[str]: + """ + returns all available temperature conversion rates + """ + return list(CONVERSIONS_PER_SECOND.keys()) + def set_temperature_conversion_rate(self, conversion_rate: str) -> bool: """ set the number of temperature conversions per second @@ -387,25 +378,51 @@ def set_temperature_conversion_rate(self, conversion_rate: str) -> bool: else: return False - def get_chip_temperature(self) -> float: + # --------------------------------------------------------------------- + # temperature measurements - internal temperature sensor + # --------------------------------------------------------------------- + + def get_its_temperature(self) -> float: """ get internal sensor temperature in °C - the datasheet guarantees a precision of +/- 2°C + the datasheet guarantees a precision of ±2°C """ with BurstHandler(i2c_bus=self._i2c_bus, i2c_adr=self._i2c_adr) as bh: - LH.error("get_chip_temperature(): %0.1f", bh.read_register(0x00)) + LH.error("get_its_temperature(): %0.1f", bh.read_register(0x00)) return float(bh.read_register(0x00)) - def get_chip_temperature_limit(self) -> float: + def get_its_temperature_limit(self) -> float: with BurstHandler(i2c_bus=self._i2c_bus, i2c_adr=self._i2c_adr) as bh: return float(bh.read_register(0x05)) - def set_chip_temperature_limit(self, value: float): + def set_its_temperature_limit(self, value: float): with BurstHandler(i2c_bus=self._i2c_bus, i2c_adr=self._i2c_adr) as bh: bh.write_register(0x05, int(value)) - def get_external_sensor_state(self) -> ExternalSensorStatus: + # --------------------------------------------------------------------- + # temperature measurements - external temperature sensor + # --------------------------------------------------------------------- + + def configure_ets(self, ets_config: ExternalTemperatureSensorConfig) -> bool: + """ + configure diode_ideality_factor and beta_compensation_factor of + the external temperature sensor + """ + dif = ets_config.diode_ideality_factor + bcf = ets_config.beta_compensation_factor + with BurstHandler(i2c_bus=self._i2c_bus, i2c_adr=self._i2c_adr) as bh: + dev_status = bh.read_register(0x02) + if not dev_status & 0b0000_0100: + LH.debug("The diode fault bit is clear.") + bh.write_register(0x17, dif) + bh.write_register(0x18, bcf) + return True + else: + LH.error("The diode fault bit is set: Sensor is faulty or missing.") + return False + + def get_ets_state(self) -> ExternalSensorStatus: # The status register 0x02 has a diode fault bit but that bit is # set only if there is an open circuit between DP-DN. # (It is NOT set if there is a short circuit between DP-DN.) @@ -422,7 +439,7 @@ def get_external_sensor_state(self) -> ExternalSensorStatus: else: raise RuntimeError(f"unexpected external sensor state (msb: 0x{msb:02X} lsb:0x{lsb:02X})") - def has_external_sensor(self) -> bool: + def has_ets(self) -> bool: # The EMC2101 has a fault bit in the status register (0x02) but # that bit is set only if there is an open circuit between DP-DN # or if it's shorted to VDD. The bit is not set if there is a @@ -431,11 +448,11 @@ def has_external_sensor(self) -> bool: with BurstHandler(i2c_bus=self._i2c_bus, i2c_adr=self._i2c_adr) as bh: return bh.read_register(0x01) != 0b0111_1111 - def get_sensor_temperature(self) -> float: + def get_ets_temperature(self) -> float: """ get external sensor temperature in °C - the datasheet guarantees a precision of +/- 1°C + the datasheet guarantees a precision of ±1°C """ with BurstHandler(i2c_bus=self._i2c_bus, i2c_adr=self._i2c_adr) as bh: msb = bh.read_register(0x01) # high byte, must be read first! @@ -445,7 +462,7 @@ def get_sensor_temperature(self) -> float: else: return math.nan - def get_sensor_low_temperature_limit(self) -> float: + def get_ets_low_temperature_limit(self) -> float: """ get upper/lower temperature alerting limit in °C """ @@ -454,7 +471,7 @@ def get_sensor_low_temperature_limit(self) -> float: lsb = bh.read_register(0x14) # low byte return convert_bytes2temperature(msb, lsb) - def set_sensor_low_temperature_limit(self, value: float) -> float: + def set_ets_low_temperature_limit(self, value: float) -> float: """ set upper/lower temperature alerting limit in °C @@ -470,7 +487,7 @@ def set_sensor_low_temperature_limit(self, value: float) -> float: else: raise ValueError(f"temperature limit out of range ({self._temp_min} ≤ x ≤ {self._temp_max}°C)") - def get_sensor_high_temperature_limit(self) -> float: + def get_ets_high_temperature_limit(self) -> float: """ get upper/lower temperature alerting limit in °C """ @@ -479,7 +496,7 @@ def get_sensor_high_temperature_limit(self) -> float: lsb = bh.read_register(0x13) # low byte return convert_bytes2temperature(msb, lsb) - def set_sensor_high_temperature_limit(self, value: float) -> float: + def set_ets_high_temperature_limit(self, value: float) -> float: """ set upper/lower temperature alerting limit in °C @@ -555,7 +572,7 @@ def write_fancfg_register(self, value: int): def read_device_registers(self) -> dict[int, int]: registers = {} with BurstHandler(i2c_bus=self._i2c_bus, i2c_adr=self._i2c_adr) as bh: - for register in DEFAULTS.keys(): + for register in DEFAULTS: registers[register] = bh.read_register(register) return registers @@ -565,25 +582,6 @@ def reset_device_registers(self): for register, value in DEFAULTS.items(): bh.write_register(register, value) - def configure_external_temperature_sensor(self, dif: int, bcf: int) -> bool: - """ - configure diode_ideality_factor and beta_compensation_factor - - parameters: - - dif = diode_ideality_factor - - bcf = beta_compensation_factor - """ - with BurstHandler(i2c_bus=self._i2c_bus, i2c_adr=self._i2c_adr) as bh: - dev_status = bh.read_register(0x02) - if not dev_status & 0b0000_0100: - LH.debug("The diode fault bit is clear.") - bh.write_register(0x17, dif) - bh.write_register(0x18, bcf) - return True - else: - LH.error("The diode fault bit is set: Sensor is faulty or missing.") - return False - def _uses_tacho_mode(self) -> bool: with BurstHandler(i2c_bus=self._i2c_bus, i2c_adr=self._i2c_adr) as bh: status_register = bh.read_register(0x03) @@ -615,3 +613,11 @@ def _convert_tach2rpm(msb: int, lsb: int) -> int | None: return rpm else: return None + + +def _get_config_register(bh: BurstHandle) -> ConfigRegister: + return parse_config_register(bh.read_register(0x03)) + + +def _set_config_register(bh: BurstHandle, config: ConfigRegister): + bh.write_register(0x03, config.as_int()) diff --git a/feeph/emc2101/ets_config.py b/feeph/emc2101/ets_config.py new file mode 100755 index 0000000..12e3c49 --- /dev/null +++ b/feeph/emc2101/ets_config.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python3 + + +class ExternalTemperatureSensorConfig: + + def __init__(self, ideality_factor: int, beta_factor: int): + """ + configure hardware-specific settings + + These settings depend on the external temperature sensor's characteristics. + """ + self.diode_ideality_factor = ideality_factor # datasheet section 6.12 + self.beta_compensation_factor = beta_factor # datasheet section 6.13 + + +# temperature sensitive transistors +ets_2n3904 = ExternalTemperatureSensorConfig(ideality_factor=0x12, beta_factor=0x08) # 2N3904 (NPN) +ets_2n3906 = ExternalTemperatureSensorConfig(ideality_factor=0x12, beta_factor=0x08) # 2N3906 (PNP) diff --git a/feeph/emc2101/pwm.py b/feeph/emc2101/pwm.py index 03de623..5e43201 100755 --- a/feeph/emc2101/pwm.py +++ b/feeph/emc2101/pwm.py @@ -10,10 +10,12 @@ import busio # type: ignore import feeph.emc2101.utilities -from feeph.emc2101.core import CONVERSIONS_PER_SECOND, Emc2101_core, ExternalSensorStatus, SpinUpDuration, SpinUpStrength +from feeph.emc2101.config_register import ConfigRegister +from feeph.emc2101.core import Emc2101 +from feeph.emc2101.ets_config import ExternalTemperatureSensorConfig, ets_2n3904 from feeph.emc2101.fan_configs import FanConfig, RpmControlMode, generic_pwm_fan -LH = logging.getLogger(__name__) +LH = logging.getLogger('feeph.emc2101') class FanSpeedUnit(Enum): @@ -47,46 +49,23 @@ def __init__(self, rpm_control_mode: RpmControlMode, pin_six_mode: PinSixMode): emc2101_default_config = DeviceConfig(rpm_control_mode=RpmControlMode.VOLTAGE, pin_six_mode=PinSixMode.ALERT) -class ExternalTemperatureSensorConfig: - - def __init__(self, ideality_factor: int, beta_factor: int): - """ - configure hardware-specific settings - - These settings depend on the external temperature sensor's characteristics. - """ - self.diode_ideality_factor = ideality_factor # datasheet section 6.12 - self.beta_compensation_factor = beta_factor # datasheet section 6.13 - - -# temperature sensitive transistors -ets_2n3904 = ExternalTemperatureSensorConfig(ideality_factor=0x12, beta_factor=0x08) # 2N3904 (NPN) -ets_2n3906 = ExternalTemperatureSensorConfig(ideality_factor=0x12, beta_factor=0x08) # 2N3906 (PNP) - - -class TemperatureLimitType(Enum): - TOO_COLD = 1 - TOO_HOT = 2 - - # TODO add convenience function to refresh state # TODO auto-refresh state every x seconds (if desired) -class Emc2101_PWM: +class Emc2101_PWM(Emc2101): def __init__(self, i2c_bus: busio.I2C, device_config: DeviceConfig = emc2101_default_config, fan_config: FanConfig = generic_pwm_fan, ets_config: ExternalTemperatureSensorConfig = ets_2n3904): # -- initialize -- - emc2101 = Emc2101_core(i2c_bus=i2c_bus) + config = ConfigRegister() # configure pin 6 # choose between alert and tacho mode if device_config.pin_six_mode == PinSixMode.ALERT: - emc2101.configure_pin_six_as_alert() - pin_six_mode = PinSixMode.ALERT + config.alt_tach = False elif device_config.pin_six_mode == PinSixMode.TACHO: - emc2101.configure_pin_six_as_tacho() - pin_six_mode = PinSixMode.TACHO + config.alt_tach = True else: raise NotImplementedError("unsupported pin 6 mode") - emc2101.configure_minimum_rpm(minimum_rpm=fan_config.minimum_rpm) + super().__init__(i2c_bus=i2c_bus, config=config) + self.configure_minimum_rpm(minimum_rpm=fan_config.minimum_rpm) self._max_rpm = fan_config.maximum_rpm # configure PWM-related settings # The supporting electric circuit, the EMC2101's configuration @@ -100,62 +79,20 @@ def __init__(self, i2c_bus: busio.I2C, device_config: DeviceConfig = emc2101_def from feeph.emc2101.scs import PWM scs = PWM(fan_config=fan_config) pwm_d, pwm_f = feeph.emc2101.utilities.calculate_pwm_factors(pwm_frequency=fan_config.pwm_frequency) - emc2101.configure_pwm_control(pwm_d=pwm_d, pwm_f=pwm_f, step_max=max(scs.get_steps())) + self.configure_pwm_control(pwm_d=pwm_d, pwm_f=pwm_f, step_max=max(scs.get_steps())) else: raise ValueError("fan has unsupported rpm control mode") # configure external temperature sensor - dif = ets_config.diode_ideality_factor - bcf = ets_config.beta_compensation_factor - emc2101.configure_external_temperature_sensor(dif=dif, bcf=bcf) + self.configure_ets(ets_config) # -- all good: set internal state -- - self._emc2101 = emc2101 self._scs = scs - self._pin_six_mode = pin_six_mode - - def get_manufacturer_id(self) -> int: - return self._emc2101.get_manufacturer_id() - - def get_product_id(self) -> int: - return self._emc2101.get_product_id() - - def get_product_revision(self) -> int | None: - return self._emc2101.get_product_revision() - - def describe_device(self): - return self._emc2101.describe_device() # --------------------------------------------------------------------- # fan speed control # --------------------------------------------------------------------- - def configure_spinup_behaviour(self, spinup_strength: SpinUpStrength, spinup_duration: SpinUpDuration, fast_mode: bool) -> bool: - """ - configure the spin-up behavior for the attached fan (duration and - strength). This helps to ensure the fan has sufficient power - available to be able to start spinning the rotor. - - EMC2101 enters the spin-up routine any time it transitions - from a minimum fan setting (00h) to a higher fan setting - - EMC2101 does not invoke the spin-up routine upon power up - - setting a strength of 0% or duration of 0s disables spin-up entirely - - Once spin-up has completed the fan speed is reduced to the programmed setting. - - Please note: Fast_mode is ignored if pin 6 is in alert mode. - """ - if self._pin_six_mode == PinSixMode.TACHO: - self._emc2101.configure_spinup_behaviour(spinup_strength=spinup_strength, spinup_duration=spinup_duration, fast_mode=fast_mode) - return True - elif self._pin_six_mode == PinSixMode.ALERT: - LH.warning("Pin 6 is in alert mode. Can't configure spinup behavior.") - return False - else: - raise RuntimeError('internal error - inconsistent state') - - def get_rpm(self) -> int | None: - return self._emc2101.get_rpm() - def get_fixed_speed(self, unit: FanSpeedUnit = FanSpeedUnit.PERCENT) -> int | None: - step = self._emc2101.get_driver_strength() + step = self.get_driver_strength() if unit == FanSpeedUnit.PERCENT: return self._scs.convert_step2percent(step) elif unit == FanSpeedUnit.RPM: @@ -179,7 +116,7 @@ def set_fixed_speed(self, value: int, unit: FanSpeedUnit = FanSpeedUnit.PERCENT, else: LH.error("Unable to process provided percentage value '%i'!", value) # read current value and return () - step_cur = self._emc2101.get_driver_strength() + step_cur = self.get_driver_strength() return self._scs.convert_step2percent(step_cur) else: raise ValueError(f"provided value {value} is out of range (0 ≤ x ≤ 100%)") @@ -192,7 +129,7 @@ def set_fixed_speed(self, value: int, unit: FanSpeedUnit = FanSpeedUnit.PERCENT, else: LH.error("Unable to process provided RPM value '%i'!", value) # read current value and return () - step_cur = self._emc2101.get_driver_strength() + step_cur = self.get_driver_strength() return self._scs.convert_step2rpm(step_cur) else: raise ValueError(f"provided value {value} is out of range (0 ≤ x ≤ {self._max_rpm}RPM)") @@ -204,7 +141,7 @@ def set_fixed_speed(self, value: int, unit: FanSpeedUnit = FanSpeedUnit.PERCENT, else: raise ValueError("unsupported value type") # apply step - self._emc2101.set_driver_strength(step) + self.set_driver_strength(step) # convert applied value back to original unit and return if unit == FanSpeedUnit.PERCENT: return self._scs.convert_step2percent(step) @@ -213,10 +150,6 @@ def set_fixed_speed(self, value: int, unit: FanSpeedUnit = FanSpeedUnit.PERCENT, else: return step - def is_lookup_table_enabled(self) -> bool: - return self._emc2101.is_lookup_table_enabled() - - # TODO unit should be checked before entering the loop def update_lookup_table(self, values: dict[int, int], unit: FanSpeedUnit = FanSpeedUnit.PERCENT) -> bool: """ populate the lookup table with the provided values and @@ -239,139 +172,4 @@ def update_lookup_table(self, values: dict[int, int], unit: FanSpeedUnit = FanSp else: LH.error("Unable to process provided value '%i'! Skipping.", value) # ------------------------------------------------------------- - return self._emc2101.update_lookup_table(values=lut_table) - - def reset_lookup_table(self): - self._emc2101.reset_lookup_table() - - # --------------------------------------------------------------------- - # temperature measurements - # --------------------------------------------------------------------- - - def get_temperature_conversion_rate(self) -> str: - """ - get the number of temperature conversions per second - """ - return self._emc2101.get_temperature_conversion_rate() - - def get_temperature_conversion_rates(self) -> list[str]: - """ - returns all available temperature conversion rates - """ - return list(CONVERSIONS_PER_SECOND.keys()) - - def set_temperature_conversion_rate(self, conversion_rate: str) -> bool: - """ - set the number of temperature conversions per second - """ - return self._emc2101.set_temperature_conversion_rate(conversion_rate=conversion_rate) - - def get_chip_temperature(self) -> float: - """ - get internal sensor temperature in °C - - the datasheet guarantees a precision of +/- 2°C - """ - return self._emc2101.get_chip_temperature() - - def get_chip_temperature_limit(self) -> float: - return self._emc2101.get_chip_temperature_limit() - - def set_chip_temperature_limit(self, value: float): - self._emc2101.set_chip_temperature_limit(value=value) - - def get_external_sensor_state(self) -> ExternalSensorStatus: - return self._emc2101.get_external_sensor_state() - - def has_external_sensor(self) -> bool: - return self._emc2101.has_external_sensor() - - def get_sensor_temperature(self) -> float: - """ - get external sensor temperature in °C - - the datasheet guarantees a precision of +/- 1°C - """ - return self._emc2101.get_sensor_temperature() - - # TODO redesign the sensor temperature limit related functions. The interface is weird. - - def get_sensor_temperature_limit(self, limit_type: TemperatureLimitType) -> float: - """ - get upper/lower temperature alerting limit in °C - """ - if limit_type == TemperatureLimitType.TOO_COLD: - return self._emc2101.get_sensor_low_temperature_limit() - elif limit_type == TemperatureLimitType.TOO_HOT: - return self._emc2101.get_sensor_high_temperature_limit() - else: - raise ValueError("invalid limit type") - - def set_sensor_temperature_limit(self, value: float, limit_type: TemperatureLimitType) -> float: - """ - set upper/lower temperature alerting limit in °C - - The fractional part has limited precision and will be clamped to the - nearest available step. The clamped value is returned to the caller. - """ - if limit_type == TemperatureLimitType.TOO_COLD: - return self._emc2101.set_sensor_low_temperature_limit(value=value) - elif limit_type == TemperatureLimitType.TOO_HOT: - return self._emc2101.set_sensor_high_temperature_limit(value=value) - else: - raise ValueError("invalid limit type") - - def force_temperature_conversion(self): - """ - performs a one-shot conversion - """ - self._emc2101.force_temperature_conversion() - - def force_temperature(self, temperature: float): - """ - force external sensor to read a specific temperature - - (this is useful to debug the lookup table) - """ - self._emc2101.force_temperature(temperature=temperature) - - def clear_temperature(self): - """ - clear a previously forced temperature reading - """ - self._emc2101.clear_temperature() - - # --------------------------------------------------------------------- - # convenience functions - # --------------------------------------------------------------------- - - def read_fancfg_register(self) -> int: - # described in datasheet section 6.16 "Fan Configuration Register" - # 0b00000000 - # ^^-- tachometer input mode - # ^---- clock frequency override - # ^----- clock select - # ^------ polarity (0 = 100->0, 1 = 0->100) - # ^------- configure lookup table (0 = on, 1 = off) - return self._emc2101.read_fancfg_register() - - def write_fancfg_register(self, value: int): - # described in datasheet section 6.16 "Fan Configuration Register" - # 0b00000000 - # ^^-- tachometer input mode - # ^---- clock frequency override - # ^----- clock select - # ^------ polarity (0 = 100->0, 1 = 0->100) - # ^------- configure lookup table (0 = on, 1 = off) - self._emc2101.write_fancfg_register(value=value) - - def read_device_registers(self) -> dict[int, int]: - return self._emc2101.read_device_registers() - - def reset_device_registers(self): - return self._emc2101.reset_device_registers() - - def configure_external_temperature_sensor(self, ets_config: ExternalTemperatureSensorConfig): - dif = ets_config.diode_ideality_factor - bcf = ets_config.beta_compensation_factor - self._emc2101.configure_external_temperature_sensor(dif=dif, bcf=bcf) + return super().update_lookup_table(values=lut_table) diff --git a/pdm.lock b/pdm.lock index 1c4ef1d..9a03ba1 100644 --- a/pdm.lock +++ b/pdm.lock @@ -5,7 +5,7 @@ groups = ["default", "dev", "tools"] strategy = ["inherit_metadata"] lock_version = "4.5.0" -content_hash = "sha256:034351c8dada9eb3791758f5f5cbfcecc61ff4d239c87de8e53c65a8f7d80419" +content_hash = "sha256:9b6753b5816abb2c960004d15f71bb149a745305f8325f15825f71c6a4f731e6" [[metadata.targets]] requires_python = ">=3.10,<3.13" @@ -150,6 +150,20 @@ files = [ {file = "astroid-3.2.4.tar.gz", hash = "sha256:0e14202810b30da1b735827f78f5157be2bbd4a7a59b7707ca0bfc2fb4c0063a"}, ] +[[package]] +name = "attrs" +version = "24.2.0" +requires_python = ">=3.7" +summary = "Classes Without Boilerplate" +groups = ["default"] +dependencies = [ + "importlib-metadata; python_version < \"3.8\"", +] +files = [ + {file = "attrs-24.2.0-py3-none-any.whl", hash = "sha256:81921eb96de3191c8258c199618104dd27ac608d9366f5e35d011eae1867ede2"}, + {file = "attrs-24.2.0.tar.gz", hash = "sha256:5cfb1b9148b5b086569baec03f20d7b6bf3bcacc9a42bebf87ffaaca362f6346"}, +] + [[package]] name = "autopep8" version = "2.3.1" @@ -539,7 +553,7 @@ files = [ [[package]] name = "mypy" -version = "1.11.1" +version = "1.11.2" requires_python = ">=3.8" summary = "Optional static typing for Python" groups = ["tools"] @@ -549,23 +563,23 @@ dependencies = [ "typing-extensions>=4.6.0", ] files = [ - {file = "mypy-1.11.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a32fc80b63de4b5b3e65f4be82b4cfa362a46702672aa6a0f443b4689af7008c"}, - {file = "mypy-1.11.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c1952f5ea8a5a959b05ed5f16452fddadbaae48b5d39235ab4c3fc444d5fd411"}, - {file = "mypy-1.11.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e1e30dc3bfa4e157e53c1d17a0dad20f89dc433393e7702b813c10e200843b03"}, - {file = "mypy-1.11.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2c63350af88f43a66d3dfeeeb8d77af34a4f07d760b9eb3a8697f0386c7590b4"}, - {file = "mypy-1.11.1-cp310-cp310-win_amd64.whl", hash = "sha256:a831671bad47186603872a3abc19634f3011d7f83b083762c942442d51c58d58"}, - {file = "mypy-1.11.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7b6343d338390bb946d449677726edf60102a1c96079b4f002dedff375953fc5"}, - {file = "mypy-1.11.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e4fe9f4e5e521b458d8feb52547f4bade7ef8c93238dfb5bbc790d9ff2d770ca"}, - {file = "mypy-1.11.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:886c9dbecc87b9516eff294541bf7f3655722bf22bb898ee06985cd7269898de"}, - {file = "mypy-1.11.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fca4a60e1dd9fd0193ae0067eaeeb962f2d79e0d9f0f66223a0682f26ffcc809"}, - {file = "mypy-1.11.1-cp311-cp311-win_amd64.whl", hash = "sha256:0bd53faf56de9643336aeea1c925012837432b5faf1701ccca7fde70166ccf72"}, - {file = "mypy-1.11.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f39918a50f74dc5969807dcfaecafa804fa7f90c9d60506835036cc1bc891dc8"}, - {file = "mypy-1.11.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0bc71d1fb27a428139dd78621953effe0d208aed9857cb08d002280b0422003a"}, - {file = "mypy-1.11.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b868d3bcff720dd7217c383474008ddabaf048fad8d78ed948bb4b624870a417"}, - {file = "mypy-1.11.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a707ec1527ffcdd1c784d0924bf5cb15cd7f22683b919668a04d2b9c34549d2e"}, - {file = "mypy-1.11.1-cp312-cp312-win_amd64.whl", hash = "sha256:64f4a90e3ea07f590c5bcf9029035cf0efeae5ba8be511a8caada1a4893f5525"}, - {file = "mypy-1.11.1-py3-none-any.whl", hash = "sha256:0624bdb940255d2dd24e829d99a13cfeb72e4e9031f9492148f410ed30bcab54"}, - {file = "mypy-1.11.1.tar.gz", hash = "sha256:f404a0b069709f18bbdb702eb3dcfe51910602995de00bd39cea3050b5772d08"}, + {file = "mypy-1.11.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d42a6dd818ffce7be66cce644f1dff482f1d97c53ca70908dff0b9ddc120b77a"}, + {file = "mypy-1.11.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:801780c56d1cdb896eacd5619a83e427ce436d86a3bdf9112527f24a66618fef"}, + {file = "mypy-1.11.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:41ea707d036a5307ac674ea172875f40c9d55c5394f888b168033177fce47383"}, + {file = "mypy-1.11.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6e658bd2d20565ea86da7d91331b0eed6d2eee22dc031579e6297f3e12c758c8"}, + {file = "mypy-1.11.2-cp310-cp310-win_amd64.whl", hash = "sha256:478db5f5036817fe45adb7332d927daa62417159d49783041338921dcf646fc7"}, + {file = "mypy-1.11.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:75746e06d5fa1e91bfd5432448d00d34593b52e7e91a187d981d08d1f33d4385"}, + {file = "mypy-1.11.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a976775ab2256aadc6add633d44f100a2517d2388906ec4f13231fafbb0eccca"}, + {file = "mypy-1.11.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cd953f221ac1379050a8a646585a29574488974f79d8082cedef62744f0a0104"}, + {file = "mypy-1.11.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:57555a7715c0a34421013144a33d280e73c08df70f3a18a552938587ce9274f4"}, + {file = "mypy-1.11.2-cp311-cp311-win_amd64.whl", hash = "sha256:36383a4fcbad95f2657642a07ba22ff797de26277158f1cc7bd234821468b1b6"}, + {file = "mypy-1.11.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:e8960dbbbf36906c5c0b7f4fbf2f0c7ffb20f4898e6a879fcf56a41a08b0d318"}, + {file = "mypy-1.11.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:06d26c277962f3fb50e13044674aa10553981ae514288cb7d0a738f495550b36"}, + {file = "mypy-1.11.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6e7184632d89d677973a14d00ae4d03214c8bc301ceefcdaf5c474866814c987"}, + {file = "mypy-1.11.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:3a66169b92452f72117e2da3a576087025449018afc2d8e9bfe5ffab865709ca"}, + {file = "mypy-1.11.2-cp312-cp312-win_amd64.whl", hash = "sha256:969ea3ef09617aff826885a22ece0ddef69d95852cdad2f60c8bb06bf1f71f70"}, + {file = "mypy-1.11.2-py3-none-any.whl", hash = "sha256:b499bc07dbdcd3de92b0a8b29fdf592c111276f6a12fe29c30f6c417dd546d12"}, + {file = "mypy-1.11.2.tar.gz", hash = "sha256:7f9993ad3e0ffdc95c2a14b66dee63729f021968bff8ad911867579c65d13a79"}, ] [[package]] diff --git a/pyproject.toml b/pyproject.toml index 98731a5..ad1747e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,6 +31,7 @@ dependencies = [ "feeph-i2c>=0.5.0,<1.0.0", "gpiod>=2.1.0,<3.0.0", "pyyaml>=6.0.0,<7.0.0", + "attrs~=24.2", ] readme = "README.md" license = {text = "GPL-3.0-or-later"} diff --git a/requirements-dev.txt b/requirements-dev.txt index 1894d85..7c51ce0 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -150,24 +150,24 @@ markupsafe==2.1.5 \ mccabe==0.7.0 \ --hash=sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325 \ --hash=sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e -mypy==1.11.1 \ - --hash=sha256:0624bdb940255d2dd24e829d99a13cfeb72e4e9031f9492148f410ed30bcab54 \ - --hash=sha256:0bc71d1fb27a428139dd78621953effe0d208aed9857cb08d002280b0422003a \ - --hash=sha256:0bd53faf56de9643336aeea1c925012837432b5faf1701ccca7fde70166ccf72 \ - --hash=sha256:2c63350af88f43a66d3dfeeeb8d77af34a4f07d760b9eb3a8697f0386c7590b4 \ - --hash=sha256:64f4a90e3ea07f590c5bcf9029035cf0efeae5ba8be511a8caada1a4893f5525 \ - --hash=sha256:7b6343d338390bb946d449677726edf60102a1c96079b4f002dedff375953fc5 \ - --hash=sha256:886c9dbecc87b9516eff294541bf7f3655722bf22bb898ee06985cd7269898de \ - --hash=sha256:a32fc80b63de4b5b3e65f4be82b4cfa362a46702672aa6a0f443b4689af7008c \ - --hash=sha256:a707ec1527ffcdd1c784d0924bf5cb15cd7f22683b919668a04d2b9c34549d2e \ - --hash=sha256:a831671bad47186603872a3abc19634f3011d7f83b083762c942442d51c58d58 \ - --hash=sha256:b868d3bcff720dd7217c383474008ddabaf048fad8d78ed948bb4b624870a417 \ - --hash=sha256:c1952f5ea8a5a959b05ed5f16452fddadbaae48b5d39235ab4c3fc444d5fd411 \ - --hash=sha256:e1e30dc3bfa4e157e53c1d17a0dad20f89dc433393e7702b813c10e200843b03 \ - --hash=sha256:e4fe9f4e5e521b458d8feb52547f4bade7ef8c93238dfb5bbc790d9ff2d770ca \ - --hash=sha256:f39918a50f74dc5969807dcfaecafa804fa7f90c9d60506835036cc1bc891dc8 \ - --hash=sha256:f404a0b069709f18bbdb702eb3dcfe51910602995de00bd39cea3050b5772d08 \ - --hash=sha256:fca4a60e1dd9fd0193ae0067eaeeb962f2d79e0d9f0f66223a0682f26ffcc809 +mypy==1.11.2 \ + --hash=sha256:06d26c277962f3fb50e13044674aa10553981ae514288cb7d0a738f495550b36 \ + --hash=sha256:36383a4fcbad95f2657642a07ba22ff797de26277158f1cc7bd234821468b1b6 \ + --hash=sha256:3a66169b92452f72117e2da3a576087025449018afc2d8e9bfe5ffab865709ca \ + --hash=sha256:41ea707d036a5307ac674ea172875f40c9d55c5394f888b168033177fce47383 \ + --hash=sha256:478db5f5036817fe45adb7332d927daa62417159d49783041338921dcf646fc7 \ + --hash=sha256:57555a7715c0a34421013144a33d280e73c08df70f3a18a552938587ce9274f4 \ + --hash=sha256:6e658bd2d20565ea86da7d91331b0eed6d2eee22dc031579e6297f3e12c758c8 \ + --hash=sha256:6e7184632d89d677973a14d00ae4d03214c8bc301ceefcdaf5c474866814c987 \ + --hash=sha256:75746e06d5fa1e91bfd5432448d00d34593b52e7e91a187d981d08d1f33d4385 \ + --hash=sha256:7f9993ad3e0ffdc95c2a14b66dee63729f021968bff8ad911867579c65d13a79 \ + --hash=sha256:801780c56d1cdb896eacd5619a83e427ce436d86a3bdf9112527f24a66618fef \ + --hash=sha256:969ea3ef09617aff826885a22ece0ddef69d95852cdad2f60c8bb06bf1f71f70 \ + --hash=sha256:a976775ab2256aadc6add633d44f100a2517d2388906ec4f13231fafbb0eccca \ + --hash=sha256:b499bc07dbdcd3de92b0a8b29fdf592c111276f6a12fe29c30f6c417dd546d12 \ + --hash=sha256:cd953f221ac1379050a8a646585a29574488974f79d8082cedef62744f0a0104 \ + --hash=sha256:d42a6dd818ffce7be66cce644f1dff482f1d97c53ca70908dff0b9ddc120b77a \ + --hash=sha256:e8960dbbbf36906c5c0b7f4fbf2f0c7ffb20f4898e6a879fcf56a41a08b0d318 mypy-extensions==1.0.0 \ --hash=sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d \ --hash=sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782 diff --git a/requirements.txt b/requirements.txt index ff91205..de9410b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -25,6 +25,9 @@ adafruit-platformdetect==3.73.0 \ adafruit-pureio==1.1.11 \ --hash=sha256:281ab2099372cc0decc26326918996cbf21b8eed694ec4764d51eefa029d324e \ --hash=sha256:c4cfbb365731942d1f1092a116f47dfdae0aef18c5b27f1072b5824ad5ea8c7c +attrs==24.2.0 \ + --hash=sha256:5cfb1b9148b5b086569baec03f20d7b6bf3bcacc9a42bebf87ffaaca362f6346 \ + --hash=sha256:81921eb96de3191c8258c199618104dd27ac608d9366f5e35d011eae1867ede2 binho-host-adapter==0.1.6 \ --hash=sha256:1e6da7a84e208c13b5f489066f05774bff1d593d0f5bf1ca149c2b8e83eae856 \ --hash=sha256:f71ca176c1e2fc1a5dce128beb286da217555c6c7c805f2ed282a6f3507ec277 diff --git a/tests/test_config_register.py b/tests/test_config_register.py new file mode 100755 index 0000000..3a2ebd4 --- /dev/null +++ b/tests/test_config_register.py @@ -0,0 +1,147 @@ +#!/usr/bin/env python3 +# pylint: disable=missing-class-docstring,missing-function-docstring,missing-module-docstring + +import unittest + +import feeph.emc2101.config_register as sut + + +class TestConfigRegister(unittest.TestCase): + + def test_init_with_defaults(self): + config = sut.ConfigRegister() + # ----------------------------------------------------------------- + computed = config.as_int() + expected = 0b0000_0000 + # ----------------------------------------------------------------- + self.assertEqual(computed, expected) + + def test_init_mask(self): + config = sut.ConfigRegister(mask=True) + # ----------------------------------------------------------------- + computed = config.as_int() + expected = 0b1000_0000 + # ----------------------------------------------------------------- + self.assertEqual(computed, expected) + + def test_init_standby(self): + config = sut.ConfigRegister(standby=True) + # ----------------------------------------------------------------- + computed = config.as_int() + expected = 0b0100_0000 + # ----------------------------------------------------------------- + self.assertEqual(computed, expected) + + def test_init_fan_standby(self): + config = sut.ConfigRegister(fan_standby=True) + # ----------------------------------------------------------------- + computed = config.as_int() + expected = 0b0010_0000 + # ----------------------------------------------------------------- + self.assertEqual(computed, expected) + + def test_init_dac(self): + config = sut.ConfigRegister(dac=True) + # ----------------------------------------------------------------- + computed = config.as_int() + expected = 0b0001_0000 + # ----------------------------------------------------------------- + self.assertEqual(computed, expected) + + def test_init_dis_to(self): + config = sut.ConfigRegister(dis_to=True) + # ----------------------------------------------------------------- + computed = config.as_int() + expected = 0b0000_1000 + # ----------------------------------------------------------------- + self.assertEqual(computed, expected) + + def test_init_alt_tach(self): + config = sut.ConfigRegister(alt_tach=True) + # ----------------------------------------------------------------- + computed = config.as_int() + expected = 0b0000_0100 + # ----------------------------------------------------------------- + self.assertEqual(computed, expected) + + def test_init_trcit_ovrd(self): + config = sut.ConfigRegister(trcit_ovrd=True) + # ----------------------------------------------------------------- + computed = config.as_int() + expected = 0b0000_0010 + # ----------------------------------------------------------------- + self.assertEqual(computed, expected) + + def test_init_queue(self): + config = sut.ConfigRegister(queue=True) + # ----------------------------------------------------------------- + computed = config.as_int() + expected = 0b0000_0001 + # ----------------------------------------------------------------- + self.assertEqual(computed, expected) + + +class TestParseConfigRegister(unittest.TestCase): + + def test_parse_defaults(self): + # ----------------------------------------------------------------- + computed = sut.parse_config_register(0b0000_0000) + expected = sut.ConfigRegister() + # ----------------------------------------------------------------- + self.assertEqual(computed, expected) + + def test_parse_mask(self): + # ----------------------------------------------------------------- + computed = sut.parse_config_register(0b1000_0000) + expected = sut.ConfigRegister(mask=True) + # ----------------------------------------------------------------- + self.assertEqual(computed, expected) + + def test_parse_standby(self): + # ----------------------------------------------------------------- + computed = sut.parse_config_register(0b0100_0000) + expected = sut.ConfigRegister(standby=True) + # ----------------------------------------------------------------- + self.assertEqual(computed, expected) + + def test_parse_fan_standby(self): + # ----------------------------------------------------------------- + computed = sut.parse_config_register(0b0010_0000) + expected = sut.ConfigRegister(fan_standby=True) + # ----------------------------------------------------------------- + self.assertEqual(computed, expected) + + def test_parse_dac(self): + # ----------------------------------------------------------------- + computed = sut.parse_config_register(0b0001_0000) + expected = sut.ConfigRegister(dac=True) + # ----------------------------------------------------------------- + self.assertEqual(computed, expected) + + def test_parse_dis_to(self): + # ----------------------------------------------------------------- + computed = sut.parse_config_register(0b0000_1000) + expected = sut.ConfigRegister(dis_to=True) + # ----------------------------------------------------------------- + self.assertEqual(computed, expected) + + def test_parse_alt_tach(self): + # ----------------------------------------------------------------- + computed = sut.parse_config_register(0b0000_0100) + expected = sut.ConfigRegister(alt_tach=True) + # ----------------------------------------------------------------- + self.assertEqual(computed, expected) + + def test_parse_trcit_ovrd(self): + # ----------------------------------------------------------------- + computed = sut.parse_config_register(0b0000_0010) + expected = sut.ConfigRegister(trcit_ovrd=True) + # ----------------------------------------------------------------- + self.assertEqual(computed, expected) + + def test_parse_queue(self): + # ----------------------------------------------------------------- + computed = sut.parse_config_register(0b0000_0001) + expected = sut.ConfigRegister(queue=True) + # ----------------------------------------------------------------- + self.assertEqual(computed, expected) diff --git a/tests/test_emc2101_core.py b/tests/test_emc2101.py similarity index 66% rename from tests/test_emc2101_core.py rename to tests/test_emc2101.py index 4396383..32be9c0 100755 --- a/tests/test_emc2101_core.py +++ b/tests/test_emc2101.py @@ -3,7 +3,6 @@ import os import unittest -from unittest.mock import MagicMock, call # modules board and busio provide no type hints import board # type: ignore @@ -40,7 +39,7 @@ def setUp(self): registers[0xFE] = 0x5D # manufacturer id registers[0xFF] = 0x02 # revision self.i2c_bus = EmulatedI2C(state={self.i2c_adr: registers}) - self.emc2101 = sut.Emc2101_core(i2c_bus=self.i2c_bus) + self.emc2101 = sut.Emc2101(i2c_bus=self.i2c_bus, config=sut.ConfigRegister()) # restore original state after each run # (hardware is not stateless) self.emc2101.reset_device_registers() @@ -50,49 +49,53 @@ def tearDown(self): pass # --------------------------------------------------------------------- - # circuit-dependent settings + # hardware details # --------------------------------------------------------------------- - def test_pin_six_as_alert(self): + def test_manufacturer_id(self): # ----------------------------------------------------------------- - computed = self.emc2101.configure_pin_six_as_alert() - expected = True + computed = self.emc2101.get_manufacturer_id() + expected = [ + 0x5D, # SMSC + ] # ----------------------------------------------------------------- - self.assertEqual(computed, expected, "Failed to enable alert mode.") - with BurstHandler(i2c_bus=self.i2c_bus, i2c_adr=self.i2c_adr) as bh: - self.assertEqual(bh.read_register(0x03), 0b0000_0000) - self.assertEqual(bh.read_register(0x4B), 0b0000_0000) + self.assertIn(computed, expected, f"Got unexpected manufacturer ID '{computed}'.") - @unittest.skipIf(HAS_HARDWARE, "Skipping forced failure test.") - def test_pin_six_as_alert_failure(self): - self.i2c_bus._lock_chance = 0 + def test_product_id(self): # ----------------------------------------------------------------- - computed = self.emc2101.configure_pin_six_as_alert() - expected = False + computed = self.emc2101.get_product_id() + expected = [ + 0x16, # EMC2101 + 0x28, # EMC2101R + ] # ----------------------------------------------------------------- - self.assertEqual(computed, expected) + self.assertIn(computed, expected, f"Got unexpected product ID '{computed}'.") - def test_pin_six_as_tacho(self): + def test_product_revision(self): # ----------------------------------------------------------------- - computed = self.emc2101.configure_pin_six_as_tacho() - expected = True + computed = self.emc2101.get_product_revision() + expected = range(0x00, 0x17) # assuming 0..22 are valid values for revision # ----------------------------------------------------------------- - self.assertEqual(computed, expected, "Failed to enable tacho mode.") - with BurstHandler(i2c_bus=self.i2c_bus, i2c_adr=self.i2c_adr) as bh: - self.assertEqual(bh.read_register(0x03), 0b0000_0100) - self.assertEqual(bh.read_register(0x4B), 0b0011_1111) + self.assertIn(computed, expected, f"Got unexpected product ID '{computed}'.") - @unittest.skipIf(HAS_HARDWARE, "Skipping forced failure test.") - def test_pin_six_as_tacho_failure(self): - self.i2c_bus._lock_chance = 0 + def test_describe_product(self): + mid = self.emc2101.get_manufacturer_id() + pid = self.emc2101.get_product_id() + rev = self.emc2101.get_product_revision() # ----------------------------------------------------------------- - computed = self.emc2101.configure_pin_six_as_tacho() - expected = False + computed = self.emc2101.describe_device() + expected = f"SMSC (0x{mid:02X}) EMC2101 (0x{pid:02X}) (rev: {rev})" # ----------------------------------------------------------------- - self.assertEqual(computed, expected) + self.assertIn(computed, expected, f"Got unexpected product ID '{computed}'.") + + # --------------------------------------------------------------------- + # circuit-dependent settings + # --------------------------------------------------------------------- def test_pin_get_rpm_in_alert_mode(self): - self.emc2101.configure_pin_six_as_alert() + # if pin 6 is used as an interrupt pin (alert mode) we can't read + # fan speed + self.emc2101.set_config_register(sut.ConfigRegister(alt_tach=False)) # ----------------------------------------------------------------- computed = self.emc2101.get_rpm() expected = None @@ -103,34 +106,77 @@ def test_pin_get_rpm_in_alert_mode(self): # fan speed settings # --------------------------------------------------------------------- - def test_configure_dac_control(self): - self.emc2101.configure_dac_control(15) - # ----------------------------------------------------------------- + def test_configure_pwm_control_1(self): with BurstHandler(i2c_bus=self.i2c_bus, i2c_adr=self.i2c_adr) as bh: - computed = bh.read_register(0x03) & 0b0001_0000 - expected = 0b0001_0000 + bh.write_register(0x03, 0b0000_0000) # pwm + bh.write_register(0x4D, 0b0001_0111) # default (0x17) + bh.write_register(0x4E, 0b0000_0001) # default (0x01) + # ----------------------------------------------------------------- + computed = self.emc2101.configure_pwm_control(pwm_d=0x12, pwm_f=0x34, step_max=15) + expected = True # ----------------------------------------------------------------- self.assertEqual(computed, expected) - self.assertEqual(self.emc2101._step_max, 15) + with BurstHandler(i2c_bus=self.i2c_bus, i2c_adr=self.i2c_adr) as bh: + self.assertEqual(bh.read_register(0x03), 0b0000_0000) + self.assertEqual(bh.read_register(0x4D), 0x34) + self.assertEqual(bh.read_register(0x4E), 0x12) - def test_configure_pwm_control(self): - self.emc2101.configure_pwm_control(pwm_d=0x12, pwm_f=0x34, step_max=15) + def test_configure_pwm_control_2(self): + with BurstHandler(i2c_bus=self.i2c_bus, i2c_adr=self.i2c_adr) as bh: + bh.write_register(0x03, 0b0001_0000) # dac + bh.write_register(0x4D, 0b0001_0111) # default (0x17) + bh.write_register(0x4E, 0b0000_0001) # default (0x01) # ----------------------------------------------------------------- + computed = self.emc2101.configure_pwm_control(pwm_d=0x12, pwm_f=0x34, step_max=15) + expected = False # ----------------------------------------------------------------- + self.assertEqual(computed, expected) with BurstHandler(i2c_bus=self.i2c_bus, i2c_adr=self.i2c_adr) as bh: - self.assertFalse(bh.read_register(0x03) & 0b0001_0000) - self.assertEqual(bh.read_register(0x4D), 0x34) - self.assertEqual(bh.read_register(0x4E), 0x12) + self.assertEqual(bh.read_register(0x03), 0b0001_0000) + self.assertEqual(bh.read_register(0x4D), 0x17) + self.assertEqual(bh.read_register(0x4E), 0x01) - def test_configure_spinup_behaviour(self): + def test_configure_spinup_behaviour_1(self): spinup_duration = sut.SpinUpDuration.TIME_0_80 # 0b...._.101 spinup_strength = sut.SpinUpStrength.STRENGTH_50 # 0b...0_1... fast_mode = False # 0b..0._.... - self.emc2101.configure_spinup_behaviour(spinup_strength=spinup_duration, spinup_duration=spinup_strength, fast_mode=fast_mode) + self.emc2101.set_config_register(config=sut.ConfigRegister(alt_tach=True)) # ----------------------------------------------------------------- + computed = self.emc2101.configure_spinup_behaviour(spinup_strength=spinup_duration, spinup_duration=spinup_strength, fast_mode=fast_mode) + expected = True # ----------------------------------------------------------------- + self.assertEqual(computed, expected) with BurstHandler(i2c_bus=self.i2c_bus, i2c_adr=self.i2c_adr) as bh: - self.assertEqual(bh.read_register(0x4B), 0b00001101) + self.assertEqual(bh.read_register(0x4B), 0b0000_1101) + + def test_configure_spinup_behaviour_2(self): + spinup_duration = sut.SpinUpDuration.TIME_0_80 # 0b...._.101 + spinup_strength = sut.SpinUpStrength.STRENGTH_50 # 0b...0_1... + fast_mode = True # 0b..1._.... + self.emc2101.set_config_register(config=sut.ConfigRegister(alt_tach=True)) + # ----------------------------------------------------------------- + computed = self.emc2101.configure_spinup_behaviour(spinup_strength=spinup_duration, spinup_duration=spinup_strength, fast_mode=fast_mode) + expected = True + # ----------------------------------------------------------------- + self.assertEqual(computed, expected) + with BurstHandler(i2c_bus=self.i2c_bus, i2c_adr=self.i2c_adr) as bh: + self.assertEqual(bh.read_register(0x4B), 0b0010_1101) + + def test_configure_spinup_behaviour_3(self): + with BurstHandler(i2c_bus=self.i2c_bus, i2c_adr=self.i2c_adr) as bh: + bh.write_register(0x03, 0b0000_0000) # alert mode + bh.write_register(0x4B, 0b0011_1111) # default (0x3F) + spinup_duration = sut.SpinUpDuration.TIME_0_80 # 0b...._.101 + spinup_strength = sut.SpinUpStrength.STRENGTH_50 # 0b...0_1... + fast_mode = True # 0b..1._.... + # ----------------------------------------------------------------- + computed = self.emc2101.configure_spinup_behaviour(spinup_strength=spinup_duration, spinup_duration=spinup_strength, fast_mode=fast_mode) + expected = False # ignore request, pin 6 is configured for alert mode + # ----------------------------------------------------------------- + self.assertEqual(computed, expected) + with BurstHandler(i2c_bus=self.i2c_bus, i2c_adr=self.i2c_adr) as bh: + self.assertEqual(bh.read_register(0x03), 0b0000_0000) + self.assertEqual(bh.read_register(0x4B), 0b0011_1111) def test_set_minimum_rpm_too_low(self): # due to the way EMC2101's registers are implemented the measured @@ -197,60 +243,32 @@ def test_update_lookup_table_temp_too_high(self): self.assertRaises(ValueError, self.emc2101.update_lookup_table, values=lut) # --------------------------------------------------------------------- - # temperature settings + # convenience functions # --------------------------------------------------------------------- - def test_set_sensor_low_temperature_limit(self): + def test_read_fancfg_register(self): # ----------------------------------------------------------------- - # ----------------------------------------------------------------- - self.assertRaises(ValueError, self.emc2101.set_sensor_low_temperature_limit, -50) - - def test_set_sensor_high_temperature_limit(self): - # ----------------------------------------------------------------- - # ----------------------------------------------------------------- - self.assertRaises(ValueError, self.emc2101.set_sensor_high_temperature_limit, 150) - - @unittest.skipIf(HAS_HARDWARE, "Skipping forced failure test.") - def test_force_external_temperature_sensor_failure(self): - with BurstHandler(i2c_bus=self.i2c_bus, i2c_adr=self.i2c_adr) as bh: - bh.write_register(0x02, 0b0000_0100) - # ----------------------------------------------------------------- - computed = self.emc2101.configure_external_temperature_sensor(dif=0x12, bcf=0x34) - expected = False + computed = self.emc2101.read_fancfg_register() + expected = 0b0010_0000 # ----------------------------------------------------------------- self.assertEqual(computed, expected) - # One Shot Register (0x0F) - # Writing to this register initiates a one shot update of the - # temperature data. Data is not relevant and is not stored. - def test_force_temperature_conversion(self): - # we use a mock since there is no other way to observe this change - self.i2c_bus.writeto = MagicMock(name='writeto') + def test_write_fancfg_register(self): + self.emc2101.write_fancfg_register(0b0110_0000) # ----------------------------------------------------------------- - self.emc2101.force_temperature_conversion() - computed = self.i2c_bus.writeto.mock_calls - expected = [ - call(address=self.i2c_adr, buffer=bytearray([0x0F, 0x00])), - ] + computed = self.emc2101.read_fancfg_register() + expected = 0b0110_0000 # ----------------------------------------------------------------- self.assertEqual(computed, expected) - def test_force_temperature(self): - # ----------------------------------------------------------------- - self.emc2101.force_temperature(21.5) - # ----------------------------------------------------------------- - with BurstHandler(i2c_bus=self.i2c_bus, i2c_adr=self.i2c_adr) as bh: - self.assertEqual(bh.read_register(0x0C), 22) - self.assertEqual(bh.read_register(0x04A), 0b0110_0000) - - def test_clear_temperature(self): + def test_read_device_registers(self): + # this test is sloppy and only compares if we get the right keys, it + # does not check if the values are correct (could be random junk) # ----------------------------------------------------------------- - self.emc2101.force_temperature(21.5) - self.emc2101.clear_temperature() + computed = self.emc2101.read_device_registers().keys() + expected = sut.DEFAULTS.keys() # ----------------------------------------------------------------- - with BurstHandler(i2c_bus=self.i2c_bus, i2c_adr=self.i2c_adr) as bh: - self.assertEqual(bh.read_register(0x0C), 0) - self.assertEqual(bh.read_register(0x04A), 0b0010_0000) + self.assertEqual(computed, expected) # --------------------------------------------------------------------- # usage errors diff --git a/tests/test_emc2101_ets.py b/tests/test_emc2101_ets.py new file mode 100755 index 0000000..d30c983 --- /dev/null +++ b/tests/test_emc2101_ets.py @@ -0,0 +1,257 @@ +#!/usr/bin/env python3 +# pylint: disable=missing-class-docstring,missing-function-docstring + +import math +import os +import unittest + +# modules board and busio provide no type hints +import board # type: ignore +import busio # type: ignore +from feeph.i2c import BurstHandler, EmulatedI2C + +import feeph.emc2101.core +import feeph.emc2101.pwm as sut # sytem under test + +if os.environ.get('TEST_EMC2101_CHIP', 'n') == 'y': + HAS_HARDWARE = True +else: + HAS_HARDWARE = False + + +# pylint: disable=too-many-public-methods,protected-access +class TestEmc2101PWM(unittest.TestCase): + + def setUp(self): + self.i2c_adr = 0x4C + if HAS_HARDWARE: + self.i2c_bus = busio.I2C(scl=board.SCL, sda=board.SDA) + else: + # initialize read/write registers + registers = feeph.emc2101.core.DEFAULTS.copy() + # add readonly registers + registers[0x00] = 0x14 # chip temperature + registers[0x01] = 0x1B # external sensor temperature (high byte) + registers[0x02] = 0x00 # status register + registers[0x0F] = 0x00 # write only register, trigger temperature conversion + registers[0x10] = 0xE0 # external sensor temperature (low byte) + registers[0x46] = 0xFF # tacho reading (low byte) + registers[0x47] = 0xFF # tacho reading (high byte) + registers[0xFD] = 0x16 # product id + registers[0xFE] = 0x5D # manufacturer id + registers[0xFF] = 0x02 # revision + self.i2c_bus = EmulatedI2C(state={self.i2c_adr: registers}) + device_config = sut.DeviceConfig(rpm_control_mode=sut.RpmControlMode.PWM, pin_six_mode=sut.PinSixMode.TACHO) + steps = { + # fmt: off + # % RPM + 3: ( 34, 409), # noqa: E201 + 4: ( 40, 479), # noqa: E201 + 5: ( 44, 526), # noqa: E201 + 6: ( 49, 591), # noqa: E201 + 7: ( 52, 629), # noqa: E201 + 8: ( 58, 697), # noqa: E201 + 9: ( 65, 785), # noqa: E201 + 10: ( 72, 868), # noqa: E201 + 11: ( 79, 950), # noqa: E201 + 12: ( 87, 1040), # noqa: E201 + 13: ( 93, 1113), # noqa: E201 + 14: (100, 1194), + # fmt: on + } + self.fan_config = sut.FanConfig(model="Mockinator 2000", pwm_frequency=22500, rpm_control_mode=sut.RpmControlMode.PWM, minimum_duty_cycle=20, maximum_duty_cycle=100, minimum_rpm=100, maximum_rpm=2000, steps=steps) + self.emc2101 = sut.Emc2101_PWM(i2c_bus=self.i2c_bus, device_config=device_config, fan_config=self.fan_config) + # restore original state after each run + # (hardware is not stateless) + self.emc2101.reset_device_registers() + + def tearDown(self): + # nothing to do + pass + + # --------------------------------------------------------------------- + # configuration + # --------------------------------------------------------------------- + + @unittest.skipIf(HAS_HARDWARE, "Skipping external sensor test.") + def test_configure_ets(self): + with BurstHandler(i2c_bus=self.i2c_bus, i2c_adr=self.i2c_adr) as bh: + bh.write_register(0x17, 0x12) + bh.write_register(0x18, 0x08) + ets_config = sut.ExternalTemperatureSensorConfig(ideality_factor=0x11, beta_factor=0x07) + # ----------------------------------------------------------------- + computed = self.emc2101.configure_ets(ets_config=ets_config) + expected = True + # ----------------------------------------------------------------- + self.assertEqual(computed, expected) + with BurstHandler(i2c_bus=self.i2c_bus, i2c_adr=self.i2c_adr) as bh: + self.assertEqual(bh.read_register(0x17), 0x11) + self.assertEqual(bh.read_register(0x18), 0x07) + + @unittest.skipIf(HAS_HARDWARE, "Skipping forced failure test.") + def test_configure_ets_missing(self): + with BurstHandler(i2c_bus=self.i2c_bus, i2c_adr=self.i2c_adr) as bh: + bh.write_register(0x02, 0b0000_0100) + bh.write_register(0x17, 0x12) + bh.write_register(0x18, 0x08) + ets_config = sut.ExternalTemperatureSensorConfig(ideality_factor=0x11, beta_factor=0x07) + # ----------------------------------------------------------------- + computed = self.emc2101.configure_ets(ets_config=ets_config) + expected = False + # ----------------------------------------------------------------- + self.assertEqual(computed, expected) + with BurstHandler(i2c_bus=self.i2c_bus, i2c_adr=self.i2c_adr) as bh: + self.assertEqual(bh.read_register(0x17), 0x12) + self.assertEqual(bh.read_register(0x18), 0x08) + + # --------------------------------------------------------------------- + # temperature measurements + # --------------------------------------------------------------------- + + @unittest.skipIf(HAS_HARDWARE, "Skipping external sensor test.") + def test_diode_present(self): + with BurstHandler(i2c_bus=self.i2c_bus, i2c_adr=self.i2c_adr) as bh: + bh.write_register(0x02, 0b0000_0000) + bh.write_register(0x01, 0b0000_1111) + bh.write_register(0x10, 0b0000_0000) + # ----------------------------------------------------------------- + computed = self.emc2101.get_ets_state() + expected = feeph.emc2101.core.ExternalSensorStatus.OK + # ----------------------------------------------------------------- + self.assertEqual(computed, expected) + + @unittest.skipIf(HAS_HARDWARE, "Skipping external sensor test.") + def test_diode_fault_1(self): + """ + open circuit between DP-DN or short circuit to VDD + - 0x02 = 0b...._.1.. + - 0x01 = 0b0111_1111 + - 0x10 = 0b0000_0000 + """ + with BurstHandler(i2c_bus=self.i2c_bus, i2c_adr=self.i2c_adr) as bh: + bh.write_register(0x02, 0b0000_0100) + bh.write_register(0x01, 0b0111_1111) + bh.write_register(0x10, 0b0000_0000) + # ----------------------------------------------------------------- + computed = self.emc2101.get_ets_state() + expected = feeph.emc2101.core.ExternalSensorStatus.FAULT1 + # ----------------------------------------------------------------- + self.assertEqual(computed, expected) + + @unittest.skipIf(HAS_HARDWARE, "Skipping external sensor test.") + def test_diode_fault_2(self): + """" + short circuit across DP-DN or short circuit to GND + - 0x02 = 0b...._.0.. + - 0x01 = 0b0111_1111 + - 0x10 = 0b1110_0000 + """ + with BurstHandler(i2c_bus=self.i2c_bus, i2c_adr=self.i2c_adr) as bh: + bh.write_register(0x02, 0b0000_0000) + bh.write_register(0x01, 0b0111_1111) + bh.write_register(0x10, 0b1110_0000) + # ----------------------------------------------------------------- + computed = self.emc2101.get_ets_state() + expected = feeph.emc2101.core.ExternalSensorStatus.FAULT2 + # ----------------------------------------------------------------- + self.assertEqual(computed, expected) + # with BurstHandler(i2c_bus=self.i2c_bus, i2c_adr=self.i2c_adr) as bh: + # self.assertFalse(bool(bh.read_register(0x02) & 0b0000_0100)) + # self.assertEqual(bh.read_register(0x01), 0b0111_1111) + # self.assertEqual(bh.read_register(0x10), 0b1110_0000) + + def test_has_ets(self): + computed = self.emc2101.has_ets() + with BurstHandler(i2c_bus=self.i2c_bus, i2c_adr=self.i2c_adr) as bh: + value = bh.read_register(0x01) + if value == 0b0111_1111: + expected = False + else: + expected = True + # ----------------------------------------------------------------- + self.assertEqual(computed, expected) + + def test_ets_temperature(self): + # ----------------------------------------------------------------- + computed = self.emc2101.get_ets_temperature() + # ----------------------------------------------------------------- + if self.emc2101.has_ets(): + # absolute maximum representable range is -64°C ≤ x < 127°C + # (nominal operating temperature range is 0°C ≤ x ≤ 85°C) + self.assertGreaterEqual(computed, -64.0, f"Got unexpected sensor temperature '{computed}'.") + self.assertLess(computed, 127.0, f"Got unexpected sensor temperature '{computed}'.") + else: + self.assertTrue(math.isnan(computed)) + + @unittest.skipIf(HAS_HARDWARE, "Skipping forced failure test.") + def test_ets_state_invalid(self): + with BurstHandler(i2c_bus=self.i2c_bus, i2c_adr=self.i2c_adr) as bh: + bh.write_register(0x01, 0b0111_1111) + bh.write_register(0x10, 0b1110_0100) + # ----------------------------------------------------------------- + # ----------------------------------------------------------------- + self.assertRaises(RuntimeError, self.emc2101.get_ets_state) + + @unittest.skipIf(HAS_HARDWARE, "Skipping forced failure test.") + def test_ets_temperature_invalid(self): + with BurstHandler(i2c_bus=self.i2c_bus, i2c_adr=self.i2c_adr) as bh: + bh.write_register(0x01, 0b0111_1111) + bh.write_register(0x10, 0b1110_0000) + # ----------------------------------------------------------------- + computed = self.emc2101.get_ets_temperature() + # ----------------------------------------------------------------- + self.assertTrue(math.isnan(computed)) + + # --------------------------------------------------------------------- + # temperature limits + # --------------------------------------------------------------------- + + def test_get_ets_low_temperature_limit(self): + with BurstHandler(i2c_bus=self.i2c_bus, i2c_adr=self.i2c_adr) as bh: + bh.write_register(0x08, 0x12) # external sensor low limit (decimal) + bh.write_register(0x14, 0b1110_0000) # external sensor low limit (fraction) + # ----------------------------------------------------------------- + computed = self.emc2101.get_ets_low_temperature_limit() + expected = 18.9 + # ----------------------------------------------------------------- + self.assertEqual(computed, expected, f"Got unexpected sensor temperature limit '{computed}'.") + + def test_set_ets_low_temperature_limit(self): + # ----------------------------------------------------------------- + computed = self.emc2101.set_ets_low_temperature_limit(5.91) + expected = 5.9 + # ----------------------------------------------------------------- + self.assertEqual(computed, expected, f"Got unexpected sensor temperature limit '{computed}'.") + with BurstHandler(i2c_bus=self.i2c_bus, i2c_adr=self.i2c_adr) as bh: + self.assertEqual(bh.read_register(0x08), 0x05) + self.assertEqual(bh.read_register(0x14), 0b1110_0000) + + def test_set_ets_low_temperature_limit_invalid(self): + # ----------------------------------------------------------------- + # ----------------------------------------------------------------- + self.assertRaises(ValueError, self.emc2101.set_ets_low_temperature_limit, -10) + + def test_get_ets_high_temperature_limit(self): + with BurstHandler(i2c_bus=self.i2c_bus, i2c_adr=self.i2c_adr) as bh: + bh.write_register(0x07, 0x54) # external sensor low limit (decimal) + bh.write_register(0x13, 0b1110_0000) # external sensor low limit (fraction) + # ----------------------------------------------------------------- + computed = self.emc2101.get_ets_high_temperature_limit() + expected = 84.9 + # ----------------------------------------------------------------- + self.assertEqual(computed, expected, f"Got unexpected sensor temperature limit '{computed}'.") + + def test_set_ets_high_temperature_limit(self): + # ----------------------------------------------------------------- + computed = self.emc2101.set_ets_high_temperature_limit(84.91) + expected = 84.9 + # ----------------------------------------------------------------- + self.assertEqual(computed, expected, f"Got unexpected sensor temperature limit '{computed}'.") + with BurstHandler(i2c_bus=self.i2c_bus, i2c_adr=self.i2c_adr) as bh: + self.assertEqual(bh.read_register(0x07), 0x54) + self.assertEqual(bh.read_register(0x13), 0b1110_0000) + + def test_set_ets_high_temperature_limit_invalid(self): + # ----------------------------------------------------------------- + # ----------------------------------------------------------------- + self.assertRaises(ValueError, self.emc2101.set_ets_high_temperature_limit, 120) diff --git a/tests/test_emc2101_its.py b/tests/test_emc2101_its.py new file mode 100755 index 0000000..ad199d6 --- /dev/null +++ b/tests/test_emc2101_its.py @@ -0,0 +1,163 @@ +#!/usr/bin/env python3 +# pylint: disable=missing-class-docstring,missing-function-docstring + +import os +import unittest +from unittest.mock import MagicMock, call + +# modules board and busio provide no type hints +import board # type: ignore +import busio # type: ignore +from feeph.i2c import BurstHandler, EmulatedI2C + +import feeph.emc2101.core +import feeph.emc2101.pwm as sut # sytem under test + +if os.environ.get('TEST_EMC2101_CHIP', 'n') == 'y': + HAS_HARDWARE = True +else: + HAS_HARDWARE = False + + +# pylint: disable=too-many-public-methods,protected-access +class TestEmc2101PWM(unittest.TestCase): + + def setUp(self): + self.i2c_adr = 0x4C + if HAS_HARDWARE: + self.i2c_bus = busio.I2C(scl=board.SCL, sda=board.SDA) + else: + # initialize read/write registers + registers = feeph.emc2101.core.DEFAULTS.copy() + # add readonly registers + registers[0x00] = 0x14 # chip temperature + registers[0x01] = 0x1B # external sensor temperature (high byte) + registers[0x02] = 0x00 # status register + registers[0x0F] = 0x00 # write only register, trigger temperature conversion + registers[0x10] = 0xE0 # external sensor temperature (low byte) + registers[0x46] = 0xFF # tacho reading (low byte) + registers[0x47] = 0xFF # tacho reading (high byte) + registers[0xFD] = 0x16 # product id + registers[0xFE] = 0x5D # manufacturer id + registers[0xFF] = 0x02 # revision + self.i2c_bus = EmulatedI2C(state={self.i2c_adr: registers}) + device_config = sut.DeviceConfig(rpm_control_mode=sut.RpmControlMode.PWM, pin_six_mode=sut.PinSixMode.TACHO) + steps = { + # fmt: off + # % RPM + 3: ( 34, 409), # noqa: E201 + 4: ( 40, 479), # noqa: E201 + 5: ( 44, 526), # noqa: E201 + 6: ( 49, 591), # noqa: E201 + 7: ( 52, 629), # noqa: E201 + 8: ( 58, 697), # noqa: E201 + 9: ( 65, 785), # noqa: E201 + 10: ( 72, 868), # noqa: E201 + 11: ( 79, 950), # noqa: E201 + 12: ( 87, 1040), # noqa: E201 + 13: ( 93, 1113), # noqa: E201 + 14: (100, 1194), + # fmt: on + } + self.fan_config = sut.FanConfig(model="Mockinator 2000", pwm_frequency=22500, rpm_control_mode=sut.RpmControlMode.PWM, minimum_duty_cycle=20, maximum_duty_cycle=100, minimum_rpm=100, maximum_rpm=2000, steps=steps) + self.emc2101 = sut.Emc2101_PWM(i2c_bus=self.i2c_bus, device_config=device_config, fan_config=self.fan_config) + # restore original state after each run + # (hardware is not stateless) + self.emc2101.reset_device_registers() + + def tearDown(self): + # nothing to do + pass + + # --------------------------------------------------------------------- + # temperature conversion settings + # --------------------------------------------------------------------- + + def test_get_temperature_conversion_rate(self): + with BurstHandler(i2c_bus=self.i2c_bus, i2c_adr=self.i2c_adr) as bh: + bh.write_register(0x04, 0b0000_0111) + # ----------------------------------------------------------------- + computed = self.emc2101.get_temperature_conversion_rate() + expected = "8" + # ----------------------------------------------------------------- + self.assertEqual(computed, expected, f"Got unexpected temperature conversion rate '{computed}'.") + + def test_set_temperature_conversion_rate(self): + # ----------------------------------------------------------------- + self.assertTrue(self.emc2101.set_temperature_conversion_rate("1/8")) + # ----------------------------------------------------------------- + with BurstHandler(i2c_bus=self.i2c_bus, i2c_adr=self.i2c_adr) as bh: + self.assertEqual(bh.read_register(0x04), 0b0000_0001) + + def test_get_temperature_conversion_rates(self): + # ----------------------------------------------------------------- + computed = sorted(self.emc2101.get_temperature_conversion_rates()) + expected = sorted(["1/16", "1/8", "1/4", "1/2", "1", "2", "4", "8", "16", "32"]) + # ----------------------------------------------------------------- + self.assertEqual(computed, expected, f"Got unexpected temperature conversion rates '{computed}'.") + + # --------------------------------------------------------------------- + # temperature measurements + # --------------------------------------------------------------------- + + def test_its_temperature(self): + # ----------------------------------------------------------------- + computed = self.emc2101.get_its_temperature() + with BurstHandler(i2c_bus=self.i2c_bus, i2c_adr=self.i2c_adr) as bh: + expected = bh.read_register(0x00) + # ----------------------------------------------------------------- + self.assertEqual(computed, expected, f"Got unexpected chip temperature '{computed}'.") + + # One Shot Register (0x0F) + # Writing to this register initiates a one shot update of the + # temperature data. Data is not relevant and is not stored. + def test_force_temperature_conversion(self): + # we use a mock since there is no other way to observe this change + self.i2c_bus.writeto = MagicMock(name='writeto') + # ----------------------------------------------------------------- + self.emc2101.force_temperature_conversion() + computed = self.i2c_bus.writeto.mock_calls + expected = [ + call(address=self.i2c_adr, buffer=bytearray([0x0F, 0x00])), + ] + # ----------------------------------------------------------------- + self.assertEqual(computed, expected) + + def test_force_temperature(self): + # ----------------------------------------------------------------- + self.emc2101.force_temperature(21.5) + # ----------------------------------------------------------------- + with BurstHandler(i2c_bus=self.i2c_bus, i2c_adr=self.i2c_adr) as bh: + self.assertEqual(bh.read_register(0x0C), 22) + self.assertEqual(bh.read_register(0x04A), 0b0110_0000) + + def test_clear_temperature(self): + # ----------------------------------------------------------------- + self.emc2101.force_temperature(21.5) + self.emc2101.clear_temperature() + # ----------------------------------------------------------------- + with BurstHandler(i2c_bus=self.i2c_bus, i2c_adr=self.i2c_adr) as bh: + self.assertEqual(bh.read_register(0x0C), 0) + self.assertEqual(bh.read_register(0x04A), 0b0010_0000) + + # --------------------------------------------------------------------- + # temperature limits + # --------------------------------------------------------------------- + + def test_its_temperature_limit_read(self): + with BurstHandler(i2c_bus=self.i2c_bus, i2c_adr=self.i2c_adr) as bh: + bh.write_register(0x05, 0x46) + # ----------------------------------------------------------------- + computed = self.emc2101.get_its_temperature_limit() + expected = 70 + # ----------------------------------------------------------------- + self.assertEqual(computed, expected, f"Got unexpected chip temperature limit '{computed}'.") + + def test_its_temperature_limit_write(self): + self.emc2101.set_its_temperature_limit(56) + # ----------------------------------------------------------------- + with BurstHandler(i2c_bus=self.i2c_bus, i2c_adr=self.i2c_adr) as bh: + computed = bh.read_register(0x05) + expected = 56 + # ----------------------------------------------------------------- + self.assertEqual(computed, expected) diff --git a/tests/test_emc2101_lut.py b/tests/test_emc2101_lut.py new file mode 100755 index 0000000..7c76457 --- /dev/null +++ b/tests/test_emc2101_lut.py @@ -0,0 +1,242 @@ +#!/usr/bin/env python3 +# pylint: disable=missing-class-docstring,missing-function-docstring + +import os +import unittest + +# modules board and busio provide no type hints +import board # type: ignore +import busio # type: ignore +from feeph.i2c import BurstHandler, EmulatedI2C + +import feeph.emc2101.core as sut # sytem under test + +if os.environ.get('TEST_EMC2101_CHIP', 'n') == 'y': + HAS_HARDWARE = True +else: + HAS_HARDWARE = False + + +# pylint: disable=too-many-public-methods,protected-access +class TestEmc2101LookupTable(unittest.TestCase): + + def setUp(self): + self.i2c_adr = 0x4C + if HAS_HARDWARE: + self.i2c_bus = busio.I2C(scl=board.SCL, sda=board.SDA) + else: + # initialize read/write registers + registers = sut.DEFAULTS.copy() + # add readonly registers + registers[0x00] = 0x14 # chip temperature + registers[0x01] = 0x1B # external sensor temperature (high byte) + registers[0x02] = 0x00 # status register + registers[0x0F] = 0x00 # write only register, trigger temperature conversion + registers[0x10] = 0xE0 # external sensor temperature (low byte) + registers[0x46] = 0xFF # tacho reading (low byte) + registers[0x47] = 0xFF # tacho reading (high byte) + registers[0xFD] = 0x16 # product id + registers[0xFE] = 0x5D # manufacturer id + registers[0xFF] = 0x02 # revision + self.i2c_bus = EmulatedI2C(state={self.i2c_adr: registers}) + self.emc2101 = sut.Emc2101(i2c_bus=self.i2c_bus, config=sut.ConfigRegister()) + # restore original state after each run + # (hardware is not stateless) + self.emc2101.reset_device_registers() + + def tearDown(self): + # nothing to do + pass + + # --------------------------------------------------------------------- + # lookup table - common functionality + # --------------------------------------------------------------------- + + def test_update_lookup_table_is_disabled(self): + with BurstHandler(i2c_bus=self.i2c_bus, i2c_adr=self.i2c_adr) as bh: + bh.write_register(0x4A, 0b0010_0011) + # ----------------------------------------------------------------- + computed = self.emc2101.is_lookup_table_enabled() + expected = False + # ----------------------------------------------------------------- + self.assertEqual(computed, expected) + + def test_update_lookup_table_is_enabled(self): + with BurstHandler(i2c_bus=self.i2c_bus, i2c_adr=self.i2c_adr) as bh: + bh.write_register(0x4A, 0b0000_0011) + # ----------------------------------------------------------------- + computed = self.emc2101.is_lookup_table_enabled() + expected = True + # ----------------------------------------------------------------- + self.assertEqual(computed, expected) + + def test_update_lookup_table_empty(self): + values = { + } + # ----------------------------------------------------------------- + computed = self.emc2101.update_lookup_table(values=values) + expected = True + # ----------------------------------------------------------------- + self.assertEqual(computed, expected) # update was performed + with BurstHandler(i2c_bus=self.i2c_bus, i2c_adr=self.i2c_adr) as bh: + self.assertEqual(bh.read_register(0x50), 0) + self.assertEqual(bh.read_register(0x51), 0x00) + self.assertEqual(bh.read_register(0x52), 0) + self.assertEqual(bh.read_register(0x53), 0x00) + self.assertEqual(bh.read_register(0x54), 0) + self.assertEqual(bh.read_register(0x55), 0x00) + self.assertEqual(bh.read_register(0x56), 0) + self.assertEqual(bh.read_register(0x57), 0x00) + self.assertEqual(bh.read_register(0x58), 0) + self.assertEqual(bh.read_register(0x59), 0x00) + self.assertEqual(bh.read_register(0x5A), 0) + self.assertEqual(bh.read_register(0x5B), 0x00) + self.assertEqual(bh.read_register(0x5C), 0) + self.assertEqual(bh.read_register(0x5D), 0x00) + self.assertEqual(bh.read_register(0x5E), 0) + self.assertEqual(bh.read_register(0x5F), 0x00) + + def test_update_lookup_table_partial(self): + # there's nothing specific decimal or hex about these values, + # using different number systems simply to make it easier to + # see what's coming from where + values = { + 16: 0x03, # temp+speed #1 + 24: 0x04, # temp+speed #2 + # the remaining 6 slots remain unused + } + # ----------------------------------------------------------------- + computed = self.emc2101.update_lookup_table(values=values) + expected = True + # ----------------------------------------------------------------- + self.assertEqual(computed, expected) # update was performed + with BurstHandler(i2c_bus=self.i2c_bus, i2c_adr=self.i2c_adr) as bh: + self.assertEqual(bh.read_register(0x50), 16) + self.assertEqual(bh.read_register(0x51), 0x03) + self.assertEqual(bh.read_register(0x52), 24) + self.assertEqual(bh.read_register(0x53), 0x04) + for offset in range(4, 16): + self.assertEqual(bh.read_register(0x50 + offset), 0x00) + + def test_update_lookup_table_full(self): + # there's nothing specific decimal or hex about these values, + # using different number systems simply to make it easier to + # see what's coming from where + values = { + 16: 0x03, # temp+speed #1 + 24: 0x04, # temp+speed #2 + 32: 0x05, # temp+speed #3 + 40: 0x06, # temp+speed #4 + 48: 0x07, # temp+speed #5 + 56: 0x08, # temp+speed #6 + 64: 0x09, # temp+speed #7 + 72: 0x0A, # temp+speed #8 + } + # ----------------------------------------------------------------- + computed = self.emc2101.update_lookup_table(values=values) + expected = True + # ----------------------------------------------------------------- + self.assertEqual(computed, expected) # update was performed + with BurstHandler(i2c_bus=self.i2c_bus, i2c_adr=self.i2c_adr) as bh: + self.assertEqual(bh.read_register(0x50), 16) + self.assertEqual(bh.read_register(0x51), 0x03) + self.assertEqual(bh.read_register(0x52), 24) + self.assertEqual(bh.read_register(0x53), 0x04) + self.assertEqual(bh.read_register(0x54), 32) + self.assertEqual(bh.read_register(0x55), 0x05) + self.assertEqual(bh.read_register(0x56), 40) + self.assertEqual(bh.read_register(0x57), 0x06) + self.assertEqual(bh.read_register(0x58), 48) + self.assertEqual(bh.read_register(0x59), 0x07) + self.assertEqual(bh.read_register(0x5A), 56) + self.assertEqual(bh.read_register(0x5B), 0x08) + self.assertEqual(bh.read_register(0x5C), 64) + self.assertEqual(bh.read_register(0x5D), 0x09) + self.assertEqual(bh.read_register(0x5E), 72) + self.assertEqual(bh.read_register(0x5F), 0x0A) + + def test_update_lookup_table_toomany(self): + # there's nothing specific decimal or hex about these values, + # using different number systems simply to make it easier to + # see what's coming from where + values = { + 16: 0x03, # temp+speed #1 + 24: 0x04, # temp+speed #2 + 32: 0x05, # temp+speed #3 + 40: 0x06, # temp+speed #4 + 48: 0x07, # temp+speed #5 + 56: 0x08, # temp+speed #6 + 64: 0x09, # temp+speed #7 + 72: 0x0A, # temp+speed #8 + 80: 0x0B, # there is no slot #9 + } + # ----------------------------------------------------------------- + # ----------------------------------------------------------------- + self.assertRaises(ValueError, self.emc2101.update_lookup_table, values=values) + + def test_update_lookup_table_inuse(self): + with BurstHandler(i2c_bus=self.i2c_bus, i2c_adr=self.i2c_adr) as bh: + # allow lookup table update + bh.write_register(0x4A, 0b0010_0000) + # clear the table + for offset in range(0, 16): + bh.write_register(0x50 + offset, 0x00) + # reenable lookup table + bh.write_register(0x4A, 0b0000_0000) + # there's nothing specific decimal or hex about these values, + # using different number systems simply to make it easier to + # see what's coming from where + values = { + 16: 0x03, # temp+speed #1 + 24: 0x04, # temp+speed #2 + # the remaining 6 slots remain unused + } + # ----------------------------------------------------------------- + computed = self.emc2101.update_lookup_table(values=values) + expected = True + # ----------------------------------------------------------------- + self.assertEqual(computed, expected) # update was performed + with BurstHandler(i2c_bus=self.i2c_bus, i2c_adr=self.i2c_adr) as bh: + self.assertEqual(bh.read_register(0x50), 16) + self.assertEqual(bh.read_register(0x51), 0x03) + self.assertEqual(bh.read_register(0x52), 24) + self.assertEqual(bh.read_register(0x53), 0x04) + for offset in range(4, 16): + self.assertEqual(bh.read_register(0x50 + offset), 0x00) + self.assertEqual(bh.read_register(0x4A), 0b0000_0000) # lut was re-enabled + + def test_update_lookup_table_too_low(self): + values = { + 16: -65, # min temp is -64 + } + # ----------------------------------------------------------------- + # ----------------------------------------------------------------- + self.assertRaises(ValueError, self.emc2101.update_lookup_table, values=values) + + def test_update_lookup_table_too_high(self): + values = { + 16: 250, # max temp is 126 + } + # ----------------------------------------------------------------- + # ----------------------------------------------------------------- + self.assertRaises(ValueError, self.emc2101.update_lookup_table, values=values) + + def test_reset_lookup(self): + with BurstHandler(i2c_bus=self.i2c_bus, i2c_adr=self.i2c_adr) as bh: + # initialize status register + bh.write_register(0x02, 0x00) + bh.write_register(0x4A, 0x20) # allow lookup table update + # populate lookup table with non-zero values + for offset in range(0, 16, 2): + temp = 20 + (offset * 4) + speed = 3 + (offset * 1) + bh.write_register(0x50 + offset, temp) + bh.write_register(0x51 + offset, speed) + # reenable lookup table + bh.write_register(0x4A, 0x00) + # ----------------------------------------------------------------- + self.emc2101.reset_lookup_table() + # ----------------------------------------------------------------- + with BurstHandler(i2c_bus=self.i2c_bus, i2c_adr=self.i2c_adr) as bh: + for offset in range(0, 16): + self.assertEqual(bh.read_register(0x50 + offset), 0x00) diff --git a/tests/test_emc2101_pwm.py b/tests/test_emc2101_pwm.py index b9e1e56..b76e1ac 100755 --- a/tests/test_emc2101_pwm.py +++ b/tests/test_emc2101_pwm.py @@ -1,10 +1,8 @@ #!/usr/bin/env python3 # pylint: disable=missing-class-docstring,missing-function-docstring -import math import os import unittest -from unittest.mock import MagicMock, call # modules board and busio provide no type hints import board # type: ignore @@ -74,6 +72,24 @@ def tearDown(self): # initialization # --------------------------------------------------------------------- + def test_configure_pin6_alert(self): + device_config = sut.DeviceConfig(rpm_control_mode=sut.RpmControlMode.PWM, pin_six_mode=sut.PinSixMode.ALERT) + emc2101 = sut.Emc2101_PWM(i2c_bus=self.i2c_bus, device_config=device_config, fan_config=self.fan_config) + # ----------------------------------------------------------------- + computed = emc2101.get_config_register() + expected = sut.ConfigRegister(alt_tach=False) + # ----------------------------------------------------------------- + self.assertEqual(computed, expected) + + def test_configure_pin6_tacho(self): + device_config = sut.DeviceConfig(rpm_control_mode=sut.RpmControlMode.PWM, pin_six_mode=sut.PinSixMode.TACHO) + emc2101 = sut.Emc2101_PWM(i2c_bus=self.i2c_bus, device_config=device_config, fan_config=self.fan_config) + # ----------------------------------------------------------------- + computed = emc2101.get_config_register() + expected = sut.ConfigRegister(alt_tach=True) + # ----------------------------------------------------------------- + self.assertEqual(computed, expected) + def test_configure_pin6_invalid(self): device_config = sut.DeviceConfig(rpm_control_mode=sut.RpmControlMode.PWM, pin_six_mode=None) # ----------------------------------------------------------------- @@ -96,91 +112,10 @@ def test_configure_control_mode_unknown(self): # ----------------------------------------------------------------- self.assertRaises(ValueError, sut.Emc2101_PWM, i2c_bus=self.i2c_bus, device_config=device_config, fan_config=fan_config) - # --------------------------------------------------------------------- - # hardware details - # --------------------------------------------------------------------- - - def test_manufacturer_id(self): - # ----------------------------------------------------------------- - computed = self.emc2101.get_manufacturer_id() - expected = [ - 0x5D, # SMSC - ] - # ----------------------------------------------------------------- - self.assertIn(computed, expected, f"Got unexpected manufacturer ID '{computed}'.") - - def test_product_id(self): - # ----------------------------------------------------------------- - computed = self.emc2101.get_product_id() - expected = [ - 0x16, # EMC2101 - 0x28, # EMC2101R - ] - # ----------------------------------------------------------------- - self.assertIn(computed, expected, f"Got unexpected product ID '{computed}'.") - - def test_product_revision(self): - # ----------------------------------------------------------------- - computed = self.emc2101.get_product_revision() - expected = range(0x00, 0x17) # assuming 0..22 are valid values for revision - # ----------------------------------------------------------------- - self.assertIn(computed, expected, f"Got unexpected product ID '{computed}'.") - - def test_describe_product(self): - mid = self.emc2101.get_manufacturer_id() - pid = self.emc2101.get_product_id() - rev = self.emc2101.get_product_revision() - # ----------------------------------------------------------------- - computed = self.emc2101.describe_device() - expected = f"SMSC (0x{mid:02X}) EMC2101 (0x{pid:02X}) (rev: {rev})" - # ----------------------------------------------------------------- - self.assertIn(computed, expected, f"Got unexpected product ID '{computed}'.") - # --------------------------------------------------------------------- # control fan speed (manually) # --------------------------------------------------------------------- - def test_configure_spinup_behaviour(self): - spinup_duration = sut.SpinUpDuration.TIME_0_80 # 0b...._.101 - spinup_strength = sut.SpinUpStrength.STRENGTH_50 # 0b...0_1... - fast_mode = True # 0b..1._.... - # ----------------------------------------------------------------- - computed = self.emc2101.configure_spinup_behaviour(spinup_strength=spinup_strength, spinup_duration=spinup_duration, fast_mode=fast_mode) - expected = True - # ----------------------------------------------------------------- - self.assertEqual(computed, expected) - with BurstHandler(i2c_bus=self.i2c_bus, i2c_adr=self.i2c_adr) as bh: - self.assertEqual(bh.read_register(0x4B), 0b0010_1101) - - def test_configure_spinup_behaviour_alert(self): - # unable to configure spinup mode if the device is pin 6 is used as - # an alert pin (pin must be in tacho mode or we can't measure speed) - device_config = sut.DeviceConfig(rpm_control_mode=sut.RpmControlMode.PWM, pin_six_mode=sut.PinSixMode.ALERT) - emc2101_alert = sut.Emc2101_PWM(i2c_bus=self.i2c_bus, device_config=device_config, fan_config=self.fan_config) - spinup_duration = sut.SpinUpDuration.TIME_0_80 # 0b...._.101 - spinup_strength = sut.SpinUpStrength.STRENGTH_50 # 0b...0_1... - fast_mode = True # 0b..1._.... - # ----------------------------------------------------------------- - computed = emc2101_alert.configure_spinup_behaviour(spinup_strength=spinup_strength, spinup_duration=spinup_duration, fast_mode=fast_mode) - expected = False - # ----------------------------------------------------------------- - self.assertEqual(computed, expected) - - def test_configure_spinup_behaviour_invalid(self): - self.emc2101._pin_six_mode = None # force an invalid state - # ----------------------------------------------------------------- - # ----------------------------------------------------------------- - self.assertRaises(RuntimeError, self.emc2101.configure_spinup_behaviour, spinup_strength=0, spinup_duration=0, fast_mode=True) - - # result of this test on hardware is unpredictable - @unittest.skipIf(HAS_HARDWARE, "Skipping RPM test.") - def test_get_rpm(self): - # ----------------------------------------------------------------- - computed = self.emc2101.get_rpm() - expected = None - # ----------------------------------------------------------------- - self.assertEqual(computed, expected) - def test_get_fixed_speed(self): # ----------------------------------------------------------------- computed = self.emc2101.get_fixed_speed(unit=sut.FanSpeedUnit.STEP) @@ -265,302 +200,14 @@ def test_duty_cycle_write_rpm_oor(self): self.assertRaises(ValueError, self.emc2101.set_fixed_speed, 2500) # --------------------------------------------------------------------- - # temperature conversion settings - # --------------------------------------------------------------------- - - def test_get_temperature_conversion_rate(self): - with BurstHandler(i2c_bus=self.i2c_bus, i2c_adr=self.i2c_adr) as bh: - bh.write_register(0x04, 0b0000_0111) - # ----------------------------------------------------------------- - computed = self.emc2101.get_temperature_conversion_rate() - expected = "8" - # ----------------------------------------------------------------- - self.assertEqual(computed, expected, f"Got unexpected temperature conversion rate '{computed}'.") - - def test_set_temperature_conversion_rate(self): - # ----------------------------------------------------------------- - self.assertTrue(self.emc2101.set_temperature_conversion_rate("1/8")) - # ----------------------------------------------------------------- - with BurstHandler(i2c_bus=self.i2c_bus, i2c_adr=self.i2c_adr) as bh: - self.assertEqual(bh.read_register(0x04), 0b0000_0001) - - def test_get_temperature_conversion_rates(self): - # ----------------------------------------------------------------- - computed = sorted(self.emc2101.get_temperature_conversion_rates()) - expected = sorted(["1/16", "1/8", "1/4", "1/2", "1", "2", "4", "8", "16", "32"]) - # ----------------------------------------------------------------- - self.assertEqual(computed, expected, f"Got unexpected temperature conversion rates '{computed}'.") - - # --------------------------------------------------------------------- - # temperature measurements (internal sensor) - # --------------------------------------------------------------------- - - def test_chip_temperature(self): - # ----------------------------------------------------------------- - computed = self.emc2101.get_chip_temperature() - with BurstHandler(i2c_bus=self.i2c_bus, i2c_adr=self.i2c_adr) as bh: - expected = bh.read_register(0x00) - # ----------------------------------------------------------------- - self.assertEqual(computed, expected, f"Got unexpected chip temperature '{computed}'.") - - # One Shot Register (0x0F) - # Writing to this register initiates a one shot update of the - # temperature data. Data is not relevant and is not stored. - def test_force_temperature_conversion(self): - # we use a mock since there is no other way to observe this change - self.i2c_bus.writeto = MagicMock(name='writeto') - # ----------------------------------------------------------------- - self.emc2101.force_temperature_conversion() - computed = self.i2c_bus.writeto.mock_calls - expected = [ - call(address=self.i2c_adr, buffer=bytearray([0x0F, 0x00])), - ] - # ----------------------------------------------------------------- - self.assertEqual(computed, expected) - - def test_force_temperature(self): - # ----------------------------------------------------------------- - self.emc2101.force_temperature(21.5) - # ----------------------------------------------------------------- - with BurstHandler(i2c_bus=self.i2c_bus, i2c_adr=self.i2c_adr) as bh: - self.assertEqual(bh.read_register(0x0C), 22) - self.assertEqual(bh.read_register(0x04A), 0b0110_0000) - - def test_clear_temperature(self): - # ----------------------------------------------------------------- - self.emc2101.force_temperature(21.5) - self.emc2101.clear_temperature() - # ----------------------------------------------------------------- - with BurstHandler(i2c_bus=self.i2c_bus, i2c_adr=self.i2c_adr) as bh: - self.assertEqual(bh.read_register(0x0C), 0) - self.assertEqual(bh.read_register(0x04A), 0b0010_0000) - - # --------------------------------------------------------------------- - # temperature limits - # --------------------------------------------------------------------- - - def test_chip_temperature_limit_read(self): - with BurstHandler(i2c_bus=self.i2c_bus, i2c_adr=self.i2c_adr) as bh: - bh.write_register(0x05, 0x46) - # ----------------------------------------------------------------- - computed = self.emc2101.get_chip_temperature_limit() - expected = 70 - # ----------------------------------------------------------------- - self.assertEqual(computed, expected, f"Got unexpected chip temperature limit '{computed}'.") - - def test_chip_temperature_limit_write(self): - self.emc2101.set_chip_temperature_limit(56) - # ----------------------------------------------------------------- - with BurstHandler(i2c_bus=self.i2c_bus, i2c_adr=self.i2c_adr) as bh: - computed = bh.read_register(0x05) - expected = 56 - # ----------------------------------------------------------------- - self.assertEqual(computed, expected) - - def test_sensor_temperature_limit_read_lower(self): - with BurstHandler(i2c_bus=self.i2c_bus, i2c_adr=self.i2c_adr) as bh: - bh.write_register(0x08, 0x12) # external sensor low limit (decimal) - bh.write_register(0x14, 0b1110_0000) # external sensor low limit (fraction) - # ----------------------------------------------------------------- - computed = self.emc2101.get_sensor_temperature_limit(limit_type=sut.TemperatureLimitType.TOO_COLD) - expected = 18.9 - # ----------------------------------------------------------------- - self.assertEqual(computed, expected, f"Got unexpected sensor temperature limit '{computed}'.") - - def test_sensor_temperature_limit_write_lower(self): - # ----------------------------------------------------------------- - computed = self.emc2101.set_sensor_temperature_limit(5.91, limit_type=sut.TemperatureLimitType.TOO_COLD) - expected = 5.9 - # ----------------------------------------------------------------- - self.assertEqual(computed, expected, f"Got unexpected sensor temperature limit '{computed}'.") - with BurstHandler(i2c_bus=self.i2c_bus, i2c_adr=self.i2c_adr) as bh: - self.assertEqual(bh.read_register(0x08), 0x05) - self.assertEqual(bh.read_register(0x14), 0b1110_0000) - - def test_sensor_temperature_limit_read_upper(self): - with BurstHandler(i2c_bus=self.i2c_bus, i2c_adr=self.i2c_adr) as bh: - bh.write_register(0x07, 0x54) # external sensor low limit (decimal) - bh.write_register(0x13, 0b1110_0000) # external sensor low limit (fraction) - # ----------------------------------------------------------------- - computed = self.emc2101.get_sensor_temperature_limit(limit_type=sut.TemperatureLimitType.TOO_HOT) - expected = 84.9 - # ----------------------------------------------------------------- - self.assertEqual(computed, expected, f"Got unexpected sensor temperature limit '{computed}'.") - - def test_sensor_temperature_limit_write_upper(self): - # ----------------------------------------------------------------- - computed = self.emc2101.set_sensor_temperature_limit(84.91, limit_type=sut.TemperatureLimitType.TOO_HOT) - expected = 84.9 - # ----------------------------------------------------------------- - self.assertEqual(computed, expected, f"Got unexpected sensor temperature limit '{computed}'.") - with BurstHandler(i2c_bus=self.i2c_bus, i2c_adr=self.i2c_adr) as bh: - self.assertEqual(bh.read_register(0x07), 0x54) - self.assertEqual(bh.read_register(0x13), 0b1110_0000) - - def test_sensor_temperature_limit_read_invalid_limit(self): - # ----------------------------------------------------------------- - # ----------------------------------------------------------------- - self.assertRaises(ValueError, self.emc2101.get_sensor_temperature_limit, limit_type='a') - - def test_sensor_temperature_limit_write_invalid_limit(self): - # ----------------------------------------------------------------- - # ----------------------------------------------------------------- - self.assertRaises(ValueError, self.emc2101.set_sensor_temperature_limit, 5.91, limit_type='a') - - # --------------------------------------------------------------------- - # temperature measurements (external sensor) + # lookup table - extended functionality # --------------------------------------------------------------------- - @unittest.skipIf(HAS_HARDWARE, "Skipping external sensor test.") - def test_diode_present(self): - with BurstHandler(i2c_bus=self.i2c_bus, i2c_adr=self.i2c_adr) as bh: - bh.write_register(0x02, 0b0000_0000) - bh.write_register(0x01, 0b0000_1111) - bh.write_register(0x10, 0b0000_0000) - # ----------------------------------------------------------------- - computed = self.emc2101.get_external_sensor_state() - expected = sut.ExternalSensorStatus.OK - # ----------------------------------------------------------------- - self.assertEqual(computed, expected) - - @unittest.skipIf(HAS_HARDWARE, "Skipping external sensor test.") - def test_diode_fault_1(self): - """ - open circuit between DP-DN or short circuit to VDD - - 0x02 = 0b...._.1.. - - 0x01 = 0b0111_1111 - - 0x10 = 0b0000_0000 - """ - with BurstHandler(i2c_bus=self.i2c_bus, i2c_adr=self.i2c_adr) as bh: - bh.write_register(0x02, 0b0000_0100) - bh.write_register(0x01, 0b0111_1111) - bh.write_register(0x10, 0b0000_0000) - # ----------------------------------------------------------------- - computed = self.emc2101.get_external_sensor_state() - expected = sut.ExternalSensorStatus.FAULT1 - # ----------------------------------------------------------------- - self.assertEqual(computed, expected) - - @unittest.skipIf(HAS_HARDWARE, "Skipping external sensor test.") - def test_diode_fault_2(self): - """" - short circuit across DP-DN or short circuit to GND - - 0x02 = 0b...._.0.. - - 0x01 = 0b0111_1111 - - 0x10 = 0b1110_0000 - """ - with BurstHandler(i2c_bus=self.i2c_bus, i2c_adr=self.i2c_adr) as bh: - bh.write_register(0x02, 0b0000_0000) - bh.write_register(0x01, 0b0111_1111) - bh.write_register(0x10, 0b1110_0000) - # ----------------------------------------------------------------- - computed = self.emc2101.get_external_sensor_state() - expected = sut.ExternalSensorStatus.FAULT2 - # ----------------------------------------------------------------- - self.assertEqual(computed, expected) - # with BurstHandler(i2c_bus=self.i2c_bus, i2c_adr=self.i2c_adr) as bh: - # self.assertFalse(bool(bh.read_register(0x02) & 0b0000_0100)) - # self.assertEqual(bh.read_register(0x01), 0b0111_1111) - # self.assertEqual(bh.read_register(0x10), 0b1110_0000) - - def test_has_sensor(self): - computed = self.emc2101.has_external_sensor() - with BurstHandler(i2c_bus=self.i2c_bus, i2c_adr=self.i2c_adr) as bh: - value = bh.read_register(0x01) - if value == 0b0111_1111: - expected = False - else: - expected = True - # ----------------------------------------------------------------- - self.assertEqual(computed, expected) - - def test_sensor_temperature(self): - # ----------------------------------------------------------------- - computed = self.emc2101.get_sensor_temperature() - # ----------------------------------------------------------------- - if self.emc2101.has_external_sensor(): - # absolute maximum representable range is -64°C ≤ x < 127°C - # (nominal operating temperature range is 0°C ≤ x ≤ 85°C) - self.assertGreaterEqual(computed, -64.0, f"Got unexpected sensor temperature '{computed}'.") - self.assertLess(computed, 127.0, f"Got unexpected sensor temperature '{computed}'.") - else: - self.assertTrue(math.isnan(computed)) - - @unittest.skipIf(HAS_HARDWARE, "Skipping forced failure test.") - def test_sensor_state_invalid(self): - with BurstHandler(i2c_bus=self.i2c_bus, i2c_adr=self.i2c_adr) as bh: - bh.write_register(0x01, 0b0111_1111) - bh.write_register(0x10, 0b1110_0100) - # ----------------------------------------------------------------- - # ----------------------------------------------------------------- - self.assertRaises(RuntimeError, self.emc2101.get_external_sensor_state) - - @unittest.skipIf(HAS_HARDWARE, "Skipping forced failure test.") - def test_sensor_temperature_invalid(self): - with BurstHandler(i2c_bus=self.i2c_bus, i2c_adr=self.i2c_adr) as bh: - bh.write_register(0x01, 0b0111_1111) - bh.write_register(0x10, 0b1110_0000) - # ----------------------------------------------------------------- - computed = self.emc2101.get_sensor_temperature() - # ----------------------------------------------------------------- - self.assertTrue(math.isnan(computed)) - - # --------------------------------------------------------------------- - # control fan speed (lookup table) - # --------------------------------------------------------------------- - - def test_update_lookup_table_is_disabled(self): - with BurstHandler(i2c_bus=self.i2c_bus, i2c_adr=self.i2c_adr) as bh: - bh.write_register(0x4A, 0b0010_0011) - # ----------------------------------------------------------------- - computed = self.emc2101.is_lookup_table_enabled() - expected = False - # ----------------------------------------------------------------- - self.assertEqual(computed, expected) - - def test_update_lookup_table_is_enabled(self): - with BurstHandler(i2c_bus=self.i2c_bus, i2c_adr=self.i2c_adr) as bh: - bh.write_register(0x4A, 0b0000_0011) - # ----------------------------------------------------------------- - computed = self.emc2101.is_lookup_table_enabled() - expected = True - # ----------------------------------------------------------------- - self.assertEqual(computed, expected) - - def test_update_lookup_table_empty(self): - values = { - } - # ----------------------------------------------------------------- - computed = self.emc2101.update_lookup_table(values=values, unit=sut.FanSpeedUnit.STEP) - expected = True - # ----------------------------------------------------------------- - self.assertEqual(computed, expected) # update was performed - with BurstHandler(i2c_bus=self.i2c_bus, i2c_adr=self.i2c_adr) as bh: - self.assertEqual(bh.read_register(0x50), 0) - self.assertEqual(bh.read_register(0x51), 0x00) - self.assertEqual(bh.read_register(0x52), 0) - self.assertEqual(bh.read_register(0x53), 0x00) - self.assertEqual(bh.read_register(0x54), 0) - self.assertEqual(bh.read_register(0x55), 0x00) - self.assertEqual(bh.read_register(0x56), 0) - self.assertEqual(bh.read_register(0x57), 0x00) - self.assertEqual(bh.read_register(0x58), 0) - self.assertEqual(bh.read_register(0x59), 0x00) - self.assertEqual(bh.read_register(0x5A), 0) - self.assertEqual(bh.read_register(0x5B), 0x00) - self.assertEqual(bh.read_register(0x5C), 0) - self.assertEqual(bh.read_register(0x5D), 0x00) - self.assertEqual(bh.read_register(0x5E), 0) - self.assertEqual(bh.read_register(0x5F), 0x00) - - def test_update_lookup_table_partial(self): - # there's nothing specific decimal or hex about these values, - # using different number systems simply to make it easier to - # see what's coming from where + def test_update_lookup_table_step(self): values = { 16: 0x03, # temp+speed #1 - 24: 0x04, # temp+speed #2 - # the remaining 6 slots remain unused + 40: 0x08, # temp+speed #2 + 72: 0x0D, # temp+speed #3 } # ----------------------------------------------------------------- computed = self.emc2101.update_lookup_table(values=values, unit=sut.FanSpeedUnit.STEP) @@ -570,113 +217,10 @@ def test_update_lookup_table_partial(self): with BurstHandler(i2c_bus=self.i2c_bus, i2c_adr=self.i2c_adr) as bh: self.assertEqual(bh.read_register(0x50), 16) self.assertEqual(bh.read_register(0x51), 0x03) - self.assertEqual(bh.read_register(0x52), 24) - self.assertEqual(bh.read_register(0x53), 0x04) - for offset in range(4, 16): - self.assertEqual(bh.read_register(0x50 + offset), 0x00) - - def test_update_lookup_table_full(self): - # there's nothing specific decimal or hex about these values, - # using different number systems simply to make it easier to - # see what's coming from where - values = { - 16: 0x03, # temp+speed #1 - 24: 0x04, # temp+speed #2 - 32: 0x05, # temp+speed #3 - 40: 0x06, # temp+speed #4 - 48: 0x07, # temp+speed #5 - 56: 0x08, # temp+speed #6 - 64: 0x09, # temp+speed #7 - 72: 0x0A, # temp+speed #8 - } - # ----------------------------------------------------------------- - computed = self.emc2101.update_lookup_table(values=values, unit=sut.FanSpeedUnit.STEP) - expected = True - # ----------------------------------------------------------------- - self.assertEqual(computed, expected) # update was performed - with BurstHandler(i2c_bus=self.i2c_bus, i2c_adr=self.i2c_adr) as bh: - self.assertEqual(bh.read_register(0x50), 16) - self.assertEqual(bh.read_register(0x51), 0x03) - self.assertEqual(bh.read_register(0x52), 24) - self.assertEqual(bh.read_register(0x53), 0x04) - self.assertEqual(bh.read_register(0x54), 32) - self.assertEqual(bh.read_register(0x55), 0x05) - self.assertEqual(bh.read_register(0x56), 40) - self.assertEqual(bh.read_register(0x57), 0x06) - self.assertEqual(bh.read_register(0x58), 48) - self.assertEqual(bh.read_register(0x59), 0x07) - self.assertEqual(bh.read_register(0x5A), 56) - self.assertEqual(bh.read_register(0x5B), 0x08) - self.assertEqual(bh.read_register(0x5C), 64) - self.assertEqual(bh.read_register(0x5D), 0x09) - self.assertEqual(bh.read_register(0x5E), 72) - self.assertEqual(bh.read_register(0x5F), 0x0A) - - def test_update_lookup_table_toomany(self): - # there's nothing specific decimal or hex about these values, - # using different number systems simply to make it easier to - # see what's coming from where - values = { - 16: 0x03, # temp+speed #1 - 24: 0x04, # temp+speed #2 - 32: 0x05, # temp+speed #3 - 40: 0x06, # temp+speed #4 - 48: 0x07, # temp+speed #5 - 56: 0x08, # temp+speed #6 - 64: 0x09, # temp+speed #7 - 72: 0x0A, # temp+speed #8 - 80: 0x0B, # there is no slot #9 - } - # ----------------------------------------------------------------- - # ----------------------------------------------------------------- - self.assertRaises(ValueError, self.emc2101.update_lookup_table, values=values, unit=sut.FanSpeedUnit.STEP) - - def test_update_lookup_table_inuse(self): - with BurstHandler(i2c_bus=self.i2c_bus, i2c_adr=self.i2c_adr) as bh: - # allow lookup table update - bh.write_register(0x4A, 0b0010_0000) - # clear the table - for offset in range(0, 16): - bh.write_register(0x50 + offset, 0x00) - # reenable lookup table - bh.write_register(0x4A, 0b0000_0000) - # there's nothing specific decimal or hex about these values, - # using different number systems simply to make it easier to - # see what's coming from where - values = { - 16: 0x03, # temp+speed #1 - 24: 0x04, # temp+speed #2 - # the remaining 6 slots remain unused - } - # ----------------------------------------------------------------- - computed = self.emc2101.update_lookup_table(values=values, unit=sut.FanSpeedUnit.STEP) - expected = True - # ----------------------------------------------------------------- - self.assertEqual(computed, expected) # update was performed - with BurstHandler(i2c_bus=self.i2c_bus, i2c_adr=self.i2c_adr) as bh: - self.assertEqual(bh.read_register(0x50), 16) - self.assertEqual(bh.read_register(0x51), 0x03) - self.assertEqual(bh.read_register(0x52), 24) - self.assertEqual(bh.read_register(0x53), 0x04) - for offset in range(4, 16): - self.assertEqual(bh.read_register(0x50 + offset), 0x00) - self.assertEqual(bh.read_register(0x4A), 0b0000_0000) # lut was re-enabled - - def test_update_lookup_table_too_low(self): - values = { - 16: -65, # min temp is -64 - } - # ----------------------------------------------------------------- - # ----------------------------------------------------------------- - self.assertRaises(ValueError, self.emc2101.update_lookup_table, values=values, unit=sut.FanSpeedUnit.STEP) - - def test_update_lookup_table_too_high(self): - values = { - 16: 250, # max temp is 126 - } - # ----------------------------------------------------------------- - # ----------------------------------------------------------------- - self.assertRaises(ValueError, self.emc2101.update_lookup_table, values=values, unit=sut.FanSpeedUnit.STEP) + self.assertEqual(bh.read_register(0x52), 40) + self.assertEqual(bh.read_register(0x53), 0x08) + self.assertEqual(bh.read_register(0x54), 72) + self.assertEqual(bh.read_register(0x55), 0x0D) def test_update_lookup_table_percent(self): values = { @@ -751,60 +295,3 @@ def test_update_lookup_table_invalid_unit(self): # ----------------------------------------------------------------- # ----------------------------------------------------------------- self.assertRaises(ValueError, self.emc2101.update_lookup_table, values=values, unit=None) - - def test_reset_lookup(self): - with BurstHandler(i2c_bus=self.i2c_bus, i2c_adr=self.i2c_adr) as bh: - # initialize status register - bh.write_register(0x02, 0x00) - bh.write_register(0x4A, 0x20) # allow lookup table update - # populate lookup table with non-zero values - for offset in range(0, 16, 2): - temp = 20 + (offset * 4) - speed = 3 + (offset * 1) - bh.write_register(0x50 + offset, temp) - bh.write_register(0x51 + offset, speed) - # reenable lookup table - bh.write_register(0x4A, 0x00) - # ----------------------------------------------------------------- - self.emc2101.reset_lookup_table() - # ----------------------------------------------------------------- - with BurstHandler(i2c_bus=self.i2c_bus, i2c_adr=self.i2c_adr) as bh: - for offset in range(0, 16): - self.assertEqual(bh.read_register(0x50 + offset), 0x00) - - # --------------------------------------------------------------------- - # convenience functions - # --------------------------------------------------------------------- - - def test_read_fancfg_register(self): - # ----------------------------------------------------------------- - computed = self.emc2101.read_fancfg_register() - expected = 0b0010_0000 - # ----------------------------------------------------------------- - self.assertEqual(computed, expected) - - def test_write_fancfg_register(self): - self.emc2101.write_fancfg_register(0b0110_0000) - # ----------------------------------------------------------------- - computed = self.emc2101.read_fancfg_register() - expected = 0b0110_0000 - # ----------------------------------------------------------------- - self.assertEqual(computed, expected) - - def test_read_device_registers(self): - # this test is sloppy and only compares if we get the right keys, it - # does not check if the values are correct (could be random junk) - # ----------------------------------------------------------------- - computed = self.emc2101.read_device_registers().keys() - expected = feeph.emc2101.core.DEFAULTS.keys() - # ----------------------------------------------------------------- - self.assertEqual(computed, expected) - - def test_configure_external_temperature_sensor(self): - etsc = sut.ExternalTemperatureSensorConfig(ideality_factor=0x11, beta_factor=0x07) - self.emc2101.configure_external_temperature_sensor(ets_config=etsc) - # ----------------------------------------------------------------- - # ----------------------------------------------------------------- - with BurstHandler(i2c_bus=self.i2c_bus, i2c_adr=self.i2c_adr) as bh: - self.assertEqual(bh.read_register(0x17), 0x11) - self.assertEqual(bh.read_register(0x18), 0x07)