def test_change_setpoint(self): self.hwc.abs_acceleration = float('inf') # Changing the setpoint while the pump is off shouldn't affect the # frequency immediately. self.parameters[24].value[0] = Uint(1234, 16) time.sleep(2 * self.hwc.step) reply = self.send(flag_bits=[]) self.assertEqual(reply.frequency, 0) # However, after sending the on command the frequency becomes equal to # the checkpoint. # Send the on command. self.send(flag_bits=[ControlBits.COMMAND, ControlBits.ON]) time.sleep(2 * self.hwc.step) # Ask for pump status. reply = self.send(flag_bits=[ControlBits.COMMAND, ControlBits.ON]) self.assertEqual(reply.frequency, 1234) # Change the setpoint to a bigger value. self.parameters[24].value[0] = Uint(4567, 16) time.sleep(2 * self.hwc.step) reply = self.send(flag_bits=[ControlBits.COMMAND, ControlBits.ON]) self.assertEqual(reply.frequency, 4567) # Change the setpoint to a smaller value. self.parameters[24].value[0] = Uint(89, 16) time.sleep(2 * self.hwc.step) reply = self.send(flag_bits=[ControlBits.COMMAND, ControlBits.ON]) self.assertEqual(reply.frequency, 89)
def dummy_parameter(number=1, name='Test parameter', indices=range(0), min_value=Uint(0, 16), max_value=Uint(65535, 16), default=Uint(0, 16), unit='', writable=True, datatype=Uint, bits=16, description='Test description.'): """Create a Parameter object where all fields take default values unless otherwise specified in the arguments. """ return Parameter(number=number, name=name, indices=indices, min_value=min_value, max_value=max_value, default=default, unit=unit, writable=writable, datatype=datatype, bits=bits, description=description)
def set_up_parameters(self): def dp(number, indexed, default): indices = range(1, 4) if indexed else range(0) return dummy_parameter(number=number, indices=indices, default=default) self.unindexed = dp(1, False, Uint(1, 16)) self.single = dp(2, True, Uint(2, 16)) self.multiple = dp(3, True, [Uint(3, 16), Uint(4, 16), Uint(5, 16)]) parameters = [self.unindexed, self.single, self.multiple] return {p.number: p for p in parameters}
def parameter_error(self): """Return the parameter error. Returns: A member of the :class:`~turboctl.telegram.codes.ParameterError` enum, or ``None``, if :attr:`parameter_mode` isn't ``'error'``. Raises: ValueError: If the error number isn't valid. """ # The error code could be easily read from parameter_value # (which could be changed to always give the value as an Uint if # parameter_mode is 'error'). This property mostly exists to aid in # debugging; __str__ displays its value, which makes it easy to see # which error has occurred without looking up the meaning of the error # code. # There's no equivalent TelegramBuilder.set_error_code method, because # it isn't really needed, and adding that bit of symmetry wouldn't be # worth the increased complexity. if self.parameter_mode != 'error': return None number = Uint(self.telegram.parameter_value).value try: return ParameterError(number) except KeyError: raise ValueError(f'invalid parameter error number: {number}')
def test_indexed_multiple(self): """Test an indexed parameter with multiple default values.""" values = [Uint(x, 16) for x in (3, 4, 5)] self.assertEqual(self.pc.parameters[3].value, values) self.read_test(self.multiple.number, value=3, index=1) self.read_test(self.multiple.number, value=4, index=2) self.read_test(self.multiple.number, value=5, index=3)
def test_default_int(self): parameter = dummy_parameter_from_line(default='10') self.assertEqual(parameter.default, Uint(10, 16)) parameter = dummy_parameter_from_line( default='-10', format_='s16') self.assertEqual(parameter.default, Sint(-10, 16))
def set_parameter_number(self, value: int): """Set the parameter number. Raises: ValueError: If there isn't a parameter with the specified number. """ self._kwargs['parameter_number'] = Uint(value, 11) return self
def __init__(self, parameters=PARAMETERS): """ __init__(parameters=PARAMETERS) Initialize a new :class:`TelegramBuilder`. Args: parameters: The object to be assigned to :attr:`parameters`. """ self.parameters = parameters # Keyword arguments used to create a telegram. self._kwargs = { 'parameter_code': Bin(4 * '0', bits=4), 'parameter_number': Uint(0, bits=11), 'parameter_index': Uint(0, bits=8), 'parameter_value': Uint(0, bits=32), 'flag_bits': Bin(16 * '0', bits=16), 'frequency': Uint(0, bits=16), 'temperature': Sint(0, bits=16), 'current': Uint(0, bits=16,), 'voltage': Uint(0, bits=16), } # parameter_value and parameter_mode are special cases. # These variables store the values given by the user, and the final # values used as arguments are only determined when the telegram is # created. # __init__ and set_parameter_value set this to a int or a float, # and from_bytes to a bytes object. self._parameter_value = 0 # __init__ and set_parameter_mode set this to a string, and from_bytes # to a Bin object. self._parameter_mode = 'none'
def test_all_maxes_are_data_or_references(self): for parameter in PARAMETERS.values(): value = parameter.max_value # Convert a string value 'P<number>' to an Uint: try: if value[0] == 'P': value = Uint(int(value[1:])) except (TypeError, ValueError): pass self.assertIsInstance(value, (Uint, Sint, Float))
def from_bytes(self, bytes_): """Read the contents of the telegram from a :class:`bytes` object. The type of :attr:`parameter_value <Telegram.parameter_value>` depends on :attr:`parameter_number <Telegram.parameter_number>`, and is assigned automatically. If *bytes_* contains a parameter number that doesn't exist, a :class:`ValueError` is raised. If the parameter isn't accessed (i.e. the parameter mode is set to ``'none'`` or code to ``'0000'``), invalid parameter numbers, such as the default value of 0, are permitted. In that case, the parameter type is set to :class:`~turboctl.telegram.datatypes.Uint`. Note that this isn't a class method; a :class:`TelegramBuilder` must first be initialized normally with :meth:`__init__`, after which this method may be called. Raises: ValueError: If *bytes_* doesn't represent a valid telegram. """ self._check_valid_telegram(bytes_) code_and_number_bits = Bin(bytes_[3:5]) self._kwargs = { 'parameter_number': Uint(code_and_number_bits[5:16]), 'parameter_index': Uint(bytes_[6]), 'flag_bits': Bin(bytes_[11:13])[::-1], 'frequency': Uint(bytes_[13:15]), 'temperature': Sint(bytes_[15:17]), 'current': Uint(bytes_[17:19]), 'voltage': Uint(bytes_[21:23]) } self._parameter_value = bytes_[7:11] self._parameter_mode = Bin(code_and_number_bits[0:4]) return self
def __bytes__(self): """Return the telegram as a :class:`bytes` object. The checksum is computed automatically and added to the end. """ bytes_ = bytes( Uint(2, 8) + Uint(22, 8) + Uint(0, 8) + self.parameter_code + Bin('0') + self.parameter_number + Uint(0, 8) + self.parameter_index + self.parameter_value + self.flag_bits[::-1] + self.frequency + self.temperature + self.current + Uint(0, 16) + self.voltage ) return bytes_ + bytes([checksum(bytes_)])
def set_parameter_index(self, value: int): """Set the parameter index.""" self._kwargs['parameter_index'] = Uint(value, bits=8) return self
def test_uint_with_negative_value_raises_ValueError(self): with self.assertRaises(ValueError): Uint(-123)
def test_indexed_default_with_a_single_value(self): parameter = dummy_parameter_from_line(number='1[1:3]', default='3') self.assertEqual(parameter.default, Uint(3, 16))
def test_indexed_default_with_multiple_values(self): parameter = dummy_parameter_from_line( number='1[1:3]', default='[3,2,1]') self.assertEqual( parameter.default, [Uint(3, 16), Uint(2, 16), Uint(1, 16)])
def set_current(self, value): """Set the current.""" self._kwargs['current'] = Uint(value, bits=16) return self
def set_frequency(self, value: int): """Set the frequency.""" self._kwargs['frequency'] = Uint(value, bits=16) return self
def test_indexed_one(self): """Test an indexed parameter with one default value.""" values = 3 * [Uint(2, 16)] self.assertEqual(self.pc.parameters[2].value, values) self.read_test(self.single.number, 2)
def test_unindexed(self): """Test an unindexed parameter.""" self.assertEqual(self.pc.parameters[1].value, [Uint(1, 16)]) self.read_test(self.unindexed.number, 1)
def set_voltage(self, value): """Set the voltage.""" self._kwargs['voltage'] = Uint(value, bits=16) return self
def test_uint_from_int_sets_attributes_correctly(self, i_and_bits): i, bits = i_and_bits uint = Uint(i, bits) self.assertEqual(uint.value, i) self.assertEqual(uint.bits, bits)
def test_bits_default_value(self): self.assertEqual(Uint(1).bits, 8) self.assertEqual(Sint(-1).bits, 8) self.assertEqual(Float(1.0).bits, 32) self.assertEqual(Float(1).bits, 32) self.assertEqual(Bin(1).bits, 8)