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_bin_from_str_sets_attributes_correctly(self, str_and_bits): s, bits = str_and_bits # Bits specified. bin_ = Bin(s, bits) self.assertEqual(bin_.value, s) self.assertEqual(bin_.bits, bits) # Bits not specified. bin_ = Bin(s) self.assertEqual(bin_.value, s) self.assertEqual(bin_.bits, len(s))
def test_negative_bits_raises_ValueError_for_bin_from_int( self, i_and_bits, negative_bits): i, _ = i_and_bits with self.assertRaises(ValueError): Bin(i, negative_bits)
def test_bin_from_int_sets_attributes_correctly(self, str_and_bits): s, bits = str_and_bits # '' is a valid 0-bit representation for 0. i = int(s, 2) if s else 0 bin_ = Bin(i, bits) self.assertEqual(bin_.value, s) self.assertEqual(bin_.bits, bits)
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 set_flag_bits(self, bits): """Set the control or status bits. *bits* should be an iterable of those :class:`~turboctl.telegram.codes.ControlBits` or :class:`~turboctl.telegram.codes.StatusBits` members that should be included in the telegram. """ bitlist = 16 * ['0'] for bit in bits: bitlist[bit.value] = '1' string = ''.join(bitlist) self._kwargs['flag_bits'] = Bin(string, bits=16) 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 test_invalid_bits_raises_ValueError_for_bin_from_int(self, i_and_bits): i, bits = i_and_bits with self.assertRaises(ValueError): Bin(i, bits)
def test_bin_from_invalid_str_raises_ValueError(self): with self.assertRaises(ValueError): Bin('10201')
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)
def test_getitem_slices_binary_value(self, obj, slice_): self.assertEqual(Bin(obj[slice_]), Bin(obj)[slice_])
def test_add_concatenates_binary_representations(self, obj1, obj2): sum_ = obj1 + obj2 sum_of_bins = Bin(obj1) + Bin(obj2) self.assertEqual(Bin(sum_), sum_of_bins)
def build(self, type_='query'): """Build and return a :class:`Telegram` object. Args: type_: ``query`` if the telegram represents a message to the pump, ``reply`` if it represents a message from the pump. Raises: :class:`ValueError` or :class:`TypeError`: If a telegram cannot be created with the specified attributes. """ # Make sure type_ is valid to avoid bugs caused by a misspelled # argument. if type_ not in ['query', 'reply']: raise ValueError(f'invalid type_: {type_}') # Determine parameter access code. none_code = '0000' error_code = ParameterResponse.ERROR.value # __init__() and set_parameter_mode set self._parameter_mode to a # string, while from_bytes() sets it to a Bin object. mode_is_none = self._parameter_mode in ['none', Bin(none_code)] mode_is_error = (type_ == 'reply' and self._parameter_mode in ['error', Bin(error_code)]) if mode_is_none: self._kwargs['parameter_code'] = Bin(none_code) elif mode_is_error: self._kwargs['parameter_code'] = Bin(error_code) else: # parameter_number must be valid if the access mode isn't 'none' # or 'error'. number = self._kwargs['parameter_number'].value try: parameter = self.parameters[number] except KeyError: raise ValueError(f'invalid parameter number: {number}') if isinstance(self._parameter_mode, Bin): # If this object was created from a Bin object, # self._parameter_mode will be a bytes object. self._kwargs['parameter_code'] = self._parameter_mode else: code = get_parameter_code( type_, self._parameter_mode, bool(parameter.indices), parameter.bits ).value self._kwargs['parameter_code'] = Bin(code) # Determine parameter value. if mode_is_none or mode_is_error: # If the mode is 'none', there is no parameter access and the # datatype doesn't matter. # If the mode is 'error', the parameter value is replaced by an # Uint error code. datatype = Uint else: datatype = parameter.datatype if isinstance(self._parameter_value, bytes): # If self._parameter_value was set by from_bytes(), # self._parameter_value will be a bytes object and bits cannot be # specified. self._kwargs['parameter_value'] = datatype(self._parameter_value) else: # If _parameter_value was specified by the user, its type should # match the type of the parameter. # The default value of 0 is accepted by all parameter types. self._kwargs['parameter_value'] = datatype(self._parameter_value, bits=32) return Telegram(**self._kwargs)