""" * Author(s): SquirtleSquadLeader * License: MIT * Purpose: * The purpose of this module is to provide easy I2C Register * access. It is inspired by the CircuitPython Register * module maintained by Adafruit. * RORegBit - Single bit Read Only * RWRegBit - Single bit Read/Write * RORegBits - Multi-bit Read Only * RWRegBits - Multi-bit Read/Write * ROReg - Single/Multi Read Only * RWReg - Single/Multi Read/Write * Notes: 1) Reference format strings below: Format C Type Standard size c char 1 b signed char 1 B unsigned char 1 h short 2 H unsigned short 2 i integer 4 I unsigned int 4 l long 4 L unsigned long 4 q long long 8 Q unsigned long long 8 f float 4 d double 8 """ from machine import I2C from struct import pack, unpack class RORegBit: def __init__( self, i2c, dev_addr, reg_addr, num_bytes, bit_location, endian="", fmt="B" ): """ Creates an :class:`RORegBit` object which allows read only access to a single bit within a register. :param i2c: I2C bus which connects the host system to the peripheral device :type kind: machine.I2C() :param dev_addr: I2C address of the device which :type dev_addr: int() :param reg_addr: Physical register address which contains the bit of interest :type reg_addr: int() :param num_bytes: Number of bytes to read :type num_bytes: int() :param bit_location: Location of bit within bitfield :type bit_locatin: int() :param endian: Endian-ness of system for which the code is intended to run on. str('') uses native Endian-ness. :type endian: str() :param fmt: Format code which is used to unpack data from bytes(). Defaults to 'B' which is good for a single 8-bit register :type fmt: int() :return: An initialized RORegBit object :rtype: RORegBit() **Quickstart: Importing and using the device** Here is an example of using the :class:`RORegBit` class. First you will need to import the following libraries: .. code-block:: python from machine import I2C from register.register import RORegBit Once this is done you must define a :class:`machine.I2C` object and then pass that to the :class:`RORegBit` object to instantiate it. .. code-block:: python i2c = I2C(0) # I2C details are project specific my_reg = RORegBit(i2c, 68, 5, 1, 5) 'my_reg' can now provide access to the :method:`__get__(). Using this method will return the value of the bit found at :param bit_location:. .. code-block:: python value = my_reg.__get__() # 0 or 1 Alternatively, a :class:`RORegBit` object(s) can be placed within another class. .. code-block:: python # import from machine import I2C from register.register import RORegBit # define I2C i2c = I2C(0) # create class with desired functionality class FooDevice: def __init__(self, i2c_bus): self._my_reg_1 = RORegBit(i2c_bus, 68, 5, 1, 5) self._my_reg_2 = RORegBit(i2c_bus, 68, 6, 1, 5) def get_my_reg1(self): return self._my_reg_1.__get__() def get_my_reg2(self): return self._my_reg_1.__get__() # invoke class object device = FooDevice(i2c) """ self._i2c = i2c self._dev_addr = dev_addr self._reg_addr = reg_addr self._num_bytes = num_bytes self._bit_location = bit_location self._endian = endian self._fmt = fmt __check_reg(self) del (i2c, dev_addr, reg_addr, num_bytes, bit_location, fmt) def __get__(self): """ :return: Returns the value of the bit located at :param bit_location: :rtype: int() """ return __getbit(self) class RWRegBit: def __init__( self, i2c, dev_addr, reg_addr, num_bytes, bit_location, endian="", fmt="B" ): """ Creates an :class:`RORegBit` object which allows read and write access to a single bit within a register. :param i2c: I2C bus which connects the host system to the peripheral device :type kind: machine.I2C() :param dev_addr: I2C address of the device which :type dev_addr: int() :param reg_addr: Physical register address which contains the bit of interest :type reg_addr: int() :param num_bytes: Number of bytes to read :type num_bytes: int() :param bit_location: Location of bit within bitfield :type bit_locatin: int() :param endian: Endian-ness of system for which the code is intended to run on. str('') uses native Endian-ness. :type endian: str() :param fmt: Format code which is used to unpack data from bytes(). Defaults to 'B' which is good for a single 8-bit register :type fmt: int() :return: An initialized RWRegBit object :rtype: RWRegBit() **Quickstart: Importing and using the device** Here is an example of using the :class:`RWRegBit` class. First you will need to import the following libraries: .. code-block:: python from machine import I2C from register.register import RWRegBit Once this is done you must define a :class:`machine.I2C` object and then pass that to the :class:`RWRegBit` object to instantiate it. .. code-block:: python i2c = I2C(0) # I2C details are project specific my_reg = RWRegBit(i2c, 68, 5, 1, 5) 'my_reg' can now provide access to the :method:`__get__() and :method:`__set__(). Using these methods will get/set the value of the bit found at :param bit_location:. .. code-block:: python my_reg.__set__(1) print(my_reg.__get__()) # prints 1 Alternatively, a :class:`RWRegBit` object(s) can be placed within another class. .. code-block:: python # import from machine import I2C from register.register import RWRegBit # define I2C i2c = I2C(0) # create class with desired functionality class FooDevice: def __init__(self, i2c_bus): self._my_reg = RORegBit(i2c_bus, 68, 5, 1, 5) def get_my_reg(self): return self._my_reg.__get__() def get_my_reg2(self, n): return self._my_reg.__set__(n) # invoke class object device = FooDevice(i2c) """ self._i2c = i2c self._dev_addr = dev_addr self._reg_addr = reg_addr self._num_bytes = num_bytes self._bit_location = bit_location self._endian = endian self._fmt = fmt __check_reg(self) self._premask, self._postmask = __calc_mask( bit_location, bit_location, num_bytes ) del (i2c, dev_addr, reg_addr, num_bytes, bit_location, endian, fmt) def __get__(self): """ :return: Returns the value of the bit located at :param bit_location: :rtype: int() """ return __getbit(self) def __set__(self, setting): """ :return: Returns 'True' if operation successful :rtype: bool() """ return __setbit(self, setting) class RORegBits: def __init__( self, i2c, dev_addr, reg_addr, num_bytes, lsb, msb, endian="", fmt="B" ): """ Creates an :class:`RORegBits` object which allows read only access to a sequential set of bits within a bitfield. :param i2c: I2C bus which connects the host system to the peripheral device :type kind: machine.I2C() :param dev_addr: I2C address of the device which :type dev_addr: int() :param reg_addr: Physical register address which contains the bit of interest :type reg_addr: int() :param num_bytes: Number of bytes to read :type num_bytes: int() :param lsb: Location of least significant bit within bitfield :type lsb: int() :param msb: Location of most significant bit within bitfield :type msb: int() :param endian: Endian-ness of system for which the code is intended to run on. str('') uses native Endian-ness. :type endian: str() :param fmt: Format code which is used to unpack data from bytes(). Defaults to 'B' which is good for a single 8-bit register :type fmt: int() :return: An initialized RORegBits object :rtype: RORegBits() **Quickstart: Importing and using the device** Here is an example of using the :class:`RORegBits` class. First you will need to import the following libraries: .. code-block:: python from machine import I2C from register.register import RORegBits Once this is done you must define a :class:`machine.I2C` object and then pass that to the :class:`RORegBits` object to instantiate it. .. code-block:: python i2c = I2C(0) # I2C details are project specific my_reg = RORegBits(i2c_bus, 68, 5, 1, 0, 2) 'my_reg' can now provide access to the :method:`__get__(). Using this method will return the value of the bit found at :param bit_location:. .. code-block:: python value = my_reg.__get__() # Returns some value from 0b000 to 0b111 Alternatively, a :class:`RORegBits` object(s) can be placed within another class. .. code-block:: python # import from machine import I2C from register.register import RORegBits # define I2C i2c = I2C(0) # create class with desired functionality class FooDevice: def __init__(self, i2c_bus): self._my_reg_1 = RORegBits(i2c_bus, 68, 5, 1, 0, 2) self._my_reg_2 = RORegBits(i2c_bus, 68, 6, 1, 3, 6) def get_my_reg1(self): return self._my_reg_1.__get__() @property def my_reg2(self): return self._my_reg_2.__get__() # invoke class object device = FooDevice(i2c) n1 = device.get_my_reg() n2 = device.my_reg2 """ self._i2c = i2c self._dev_addr = dev_addr self._reg_addr = reg_addr self._num_bytes = num_bytes self._endian = endian self._fmt = fmt __check_reg(self) self._premask, self._mask, self._postmask = __calc_mask(lsb, msb, num_bytes) del (i2c, dev_addr, reg_addr, num_bytes, lsb, msb, endian, fmt) def __get__(self): """ :return: Returns the value of the bitfield located between :param lsb: and :param msb: :rtype: int() """ return __getbits(self) class RWRegBits: def __init__( self, i2c, dev_addr, reg_addr, num_bytes, lsb, msb, endian="", fmt="B" ): """ Creates an :class:`RWRegBits` object which allows read and write access to a sequential set of bits within a bitfield. :param i2c: I2C bus which connects the host system to the peripheral device :type kind: machine.I2C() :param dev_addr: I2C address of the device which :type dev_addr: int() :param reg_addr: Physical register address which contains the bit of interest :type reg_addr: int() :param num_bytes: Number of bytes to read :type num_bytes: int() :param lsb: Location of least significant bit within bitfield :type lsb: int() :param msb: Location of most significant bit within bitfield :type msb: int() :param endian: Endian-ness of system for which the code is intended to run on. str('') uses native Endian-ness. :type endian: str() :param fmt: Format code which is used to unpack data from bytes(). Defaults to 'B' which is good for a single 8-bit register :type fmt: int() :return: An initialized RWRegBits object :rtype: RWRegBits() **Quickstart: Importing and using the device** Here is an example of using the :class:`RWRegBits` class. First you will need to import the following libraries: .. code-block:: python from machine import I2C from register.register import RWRegBits Once this is done you must define a :class:`machine.I2C` object and then pass that to the :class:`RWRegBits` object to instantiate it. .. code-block:: python i2c = I2C(0) # I2C details are project specific my_reg = RWRegBits(i2c_bus, 68, 5, 1, 0, 2) 'my_reg' can now provide access to :method:`__get__() and :method:`__set__(). .. code-block:: python my_reg.__set__(0b110) # Returns some value from 0b000 to 0b111 value = my_reg.__get__() # Returns 0b110, assuming nothing changes Alternatively, a :class:`RWRegBits` object(s) can be placed within another class. .. code-block:: python # import from machine import I2C from register.register import RWRegBits # define I2C i2c = I2C(0) # create class with desired functionality class FooDevice: def __init__(self, i2c_bus): self._my_reg_1 = RWRegBits(i2c_bus, 68, 5, 1, 0, 2) self._my_reg_2 = RWRegBits(i2c_bus, 68, 6, 1, 3, 6) def get_my_reg1(self): return self._my_reg_1.__get__() def set_my_reg1(self, n): return self._my_reg_1.__set__(n) @property def my_reg2(self): return self._my_reg_2.__get__() @my_reg2.setter def my_reg2(self, n): return self._my_reg_2.__set__(n) # invoke class object device = FooDevice(i2c) device.set_my_reg(0b110) print(device.get_my_reg()) # prints 6 device.my_reg2 = 0b110 print(device.my_reg2) # prints 6 """ self._i2c = i2c self._dev_addr = dev_addr self._reg_addr = reg_addr self._num_bytes = num_bytes self._endian = endian self._fmt = fmt __check_reg(self) self._premask, self._mask, self._postmask = __calc_mask(lsb, msb, num_bytes) del (i2c, dev_addr, reg_addr, num_bytes, lsb, msb, fmt, endian) def __get__(self): """ :return: Returns the value of the bitfield located between :param lsb: and :param msb: :rtype: int() """ return __getbits(self) def __set__(self, setting): """ :return: True if successful :rtype: bool() """ return __setbits(self, setting) class ROReg: def __init__(self, i2c, dev_addr, reg_addr, num_bytes=1, endian="", fmt="B"): """ Creates a :class:`ROReg` object which allows read only access to n number of sequential registers, where n is specified by :param num_bytes:. :param i2c: I2C bus which connects the host system to the peripheral device :type kind: machine.I2C() :param dev_addr: I2C address of the device which :type dev_addr: int() :param reg_addr: Physical register address which contains the bit of interest :type reg_addr: int() :param num_bytes: Number of bytes to read. Defaults to 1. :type num_bytes: int() :param fmt: Format code which is used to unpack data from bytes(). Defaults to 'B'. :type fmt: int() :return: An initialized ROReg object :rtype: ROReg() **Quickstart: Importing and using the device** Here is an example of using the :class:`ROReg` class. First you will need to import the following libraries: .. code-block:: python from machine import I2C from register.register import ROReg Once this is done you must define a :class:`machine.I2C` object and then pass that to the :class:`ROReg` object to instantiate it. .. code-block:: python i2c = I2C(0) # I2C details are project specific my_reg = ROReg(i2c, 68, 5) 'my_reg' can now provide access to the :method:`__get__(). Using this method will return the value of the bit found at :param bit_location:. .. code-block:: python value = my_reg.__get__() # some value between 0b0 and 0b1111_1111 Alternatively, a :class:`ROReg` object(s) can be placed within another class. .. code-block:: python # import from machine import I2C from register.register import ROReg # define I2C i2c = I2C(0) # create class with desired functionality class FooDevice: def __init__(self, i2c_bus): self._my_reg_1 = ROReg(i2c_bus, 68, 5) self._my_reg_2 = ROReg(i2c_bus, 68, 6) def get_my_reg1(self): return self._my_reg_1.__get__() @property def my_reg2(self): return self._my_reg_1.__get__() # invoke class object device = FooDevice(i2c) print(device.get_my_reg1()) print(device.my_reg2) """ self._i2c = i2c self._dev_addr = dev_addr self._reg_addr = reg_addr self._num_bytes = num_bytes self._fmt = fmt self._endian = endian __check_reg(self) del (i2c, dev_addr, reg_addr, num_bytes, fmt, endian) def __get__(self): """ :return: Returns tuple containing n number of elements, where n is the number of characters in :param fmt: :rtype: tuple() """ return __getreg(self) class RWReg: def __init__(self, i2c, dev_addr, reg_addr, num_bytes, endian="", fmt="B"): """ Creates a :class:`RWReg` object which allows read and write access to n number of sequential registers, where n is specified by :param num_bytes:. :param i2c: I2C bus which connects the host system to the peripheral device :type kind: machine.I2C() :param dev_addr: I2C address of the device which :type dev_addr: int() :param reg_addr: Physical register address which contains the bit of interest :type reg_addr: int() :param num_bytes: Number of bytes to read. Defaults to 1. :type num_bytes: int() :param fmt: Format code which is used to unpack data from bytes(). Defaults to 'B'. :type fmt: int() :return: An initialized RWReg object :rtype: RWReg() **Quickstart: Importing and using the device** Here is an example of using the :class:`RWReg` class. First you will need to import the following libraries: .. code-block:: python from machine import I2C from register.register import RWReg Once this is done you must define a :class:`machine.I2C` object and then pass that to the :class:`RWReg` object to instantiate it. .. code-block:: python i2c = I2C(0) # I2C details are project specific my_reg = RWReg(i2c, 68, 5) 'my_reg' can now provide access to the :method:`__get__() and __set__(). .. code-block:: python my_reg.__set__(0b0) value = my_reg.__get__() # 0b0 if nothing changed Alternatively, a :class:`RWReg` object(s) can be placed within another class. .. code-block:: python # import from machine import I2C from register.register import RWReg # define I2C i2c = I2C(0) # create class with desired functionality class FooDevice: def __init__(self, i2c_bus): self._my_reg_1 = RWReg(i2c_bus, 68, 5) self._my_reg_2 = RWReg(i2c_bus, 68, 6) def get_my_reg1(self): return self._my_reg_1.__get__() def set_my_reg1(self, n): return self._my_reg_1.__set__(n) @property def my_reg2(self): return self._my_reg_1.__get__() @my_reg2.setter def my_reg2(self, n): return self._my_reg_1.__set__(n) # invoke class object device = FooDevice(i2c) device.set_my_reg1(0b110) print(device.get_my_reg1()) # prints 6, assuming nothing changed device.my_reg2 = 0b1111_0000 print(device.my_reg2) # prints 240 """ self._i2c = i2c self._dev_addr = dev_addr self._reg_addr = reg_addr self._num_bytes = num_bytes self._fmt = fmt self._endian = endian __check_reg(self) del (i2c, dev_addr, reg_addr, num_bytes, fmt, endian) def __get__(self): """ :return: Returns tuple containing n number of elements, where n is the number of characters in :param fmt: :rtype: tuple() """ return __getreg(self) def __set__(self, setting): """ :param setting: Value(s) to be written to register(s). Order must match :param fmt:. :type setting: int(), bytes(), bytearray(), or list/tuple containing those values in order :return: Returns True if operation successful :rtype: tuple() """ return __setreg(self, setting) """ * * GLOBAL HELPER FUNCTIONS * * """ def __getbit(reg_object): if isinstance(reg_object, (RORegBit, RWRegBit)): # Retrieve register value and unpack to int value = reg_object._i2c.readfrom_mem( reg_object._dev_addr, reg_object._reg_addr, reg_object._num_bytes ) # Unpack byte value = unpack(reg_object._endian + reg_object._fmt, value)[0] # Perform shift followed by _AND_ operation to determine bit state return (value >> reg_object._bit_location) & 0b1 else: raise TypeError("incorrect object type - must be RORegBit, RWRegBit") def __setbit(reg_object, setting): if isinstance(reg_object, RWRegBit): if setting in (0, 1): # Retrieve register value and unpack to int value = reg_object._i2c.readfrom_mem( reg_object._dev_addr, reg_object._reg_addr, reg_object._num_bytes ) # Unpack byte value = unpack(reg_object._endian + reg_object._fmt, value)[0] # Assemble byte value = ( (value & reg_object._postmask) + (setting << reg_object._bit_location) + (value & reg_object._premask) ) # Pack to bytes value = pack(reg_object._endian + reg_object._fmt, value) # Write to I2C reg_object._i2c.writeto_mem( reg_object._dev_addr, reg_object._reg_addr, value ) # Return True for success return True else: raise ValueError("setting must be int(0) or int(1)") else: raise TypeError("incorrect object type - must be RWRegBit") def __getbits(reg_object): if isinstance(reg_object, (RORegBits, RWRegBits)): # Retrieve register value and unpack to int value = reg_object._i2c.readfrom_mem( reg_object._dev_addr, reg_object._reg_addr, reg_object._num_bytes ) # Unpack bytes value = unpack(reg_object._endian + reg_object._fmt, value)[0] # Return value of bit field return (value & reg_object._mask) >> reg_object._lsb else: raise TypeError("incorrect object type - must be RORegBits, RWRegBits") def __setbits(reg_object, setting): if isinstance(reg_object, RWRegBits): if isinstance(setting, int) and setting <= reg_object._mask: # Retrieve register value and unpack to int value = reg_object._i2c.readfrom_mem( reg_object._dev_addr, reg_object._reg_addr, reg_object._num_bytes ) # Unpack bytes value = unpack(reg_object._endian + reg_object._fmt, value)[0] # Assemble value = ( (value & reg_object._postmask) + (setting << reg_object._lsb) + (value & reg_object._premask) ) # Pack to bytes object value = struct.pack(reg_object._endian + reg_object._fmt, value) # Write to device reg_object._i2c.writeto_mem( reg_object._dev_addr, reg_object._reg_addr, value ) return True else: raise ValueError( f"value of setting exceeds max value of bitfield: {reg_object._mask}" ) else: raise TypeError("incorrect object type - must be RWRegBits") def __getreg(reg_object): if isinstance(reg_object, (ROReg, RWReg)): # Retrieve register value and unpack to int values = reg_object._i2c.readfrom_mem( reg_object._dev_addr, reg_object._reg_addr, reg_object._num_bytes ) # Return Tuple of values return unpack(reg_object._endian + reg_object._fmt, values) else: raise TypeError("incorrect object type - must be ROReg, RWReg") def __setreg(reg_object, settings): if isinstance(reg_object, RWReg): if isinstance(settings, (bytes, bytearray)): # Write to device reg_object._i2c.writeto_mem( reg_object._dev_addr, reg_object._reg_addr, settings ) elif isinstance(settings, (tuple, list)): # Where our data will go d = bytearray() # Pack and append to d for n in range(0, len(settings)): d.extend(pack(reg_object._endian + reg_object._fmt[n], settings[n])) # Write to device reg_object._i2c.writeto_mem(reg_object._dev_addr, reg_object._reg_addr, d) # Assumed single int() for single reg-op elif isinstance(settings, int): d = pack(reg_object._endian + reg_object._fmt, settings) reg_object._i2c.writeto_mem(reg_object._dev_addr, reg_object._reg_addr, d) else: raise TypeError( "unsupported object type, settings must be int(), bytes(), bytearray(), tuple(), or list()" ) else: raise TypeError("incorrect object type - must be ROReg, RWReg") def __calc_mask(lsb, msb, numbytes): """ Takes in full description of bitfield that needs masking returns ints() pre, mask, post """ # Check input types if lsb.__class__() == int() and lsb >= 0: if msb.__class__() == int() and msb >= 0: if numbytes.__class__() == int() and numbytes >= 0: # Check for detectable errors if msb >= lsb: # Single bit mask if msb == lsb: pre, post = 0b0, 0b0 # Calc post masking for bit in range(msb + 1, numbytes * 8): post = (post << 1) + 0b1 # Calc pre masking for bit in range(0, lsb): pre = (pre << 1) + 0b1 return pre, post # Multibit mask else: # Values to return pre, mask, post = 0b0, 0b0, 0b0 # Calc post masking for bit in range(msb + 1, numbytes * 8): post = (post << 1) + 0b1 # Calc bitfield masking for bit in range(lsb, msb + 1): mask = (mask << 1) + 0b1 # No bits lower than 0 if lsb == 0: return 0b0, mask, post else: for bit in range(0, lsb): pre = (pre << 1) + 0b1 return pre, mask, post else: raise ValueError("msb must be greater than or equal to lsb") else: raise ValueError("numbytes must be of type int() and 0 or greater") else: raise ValueError("msb must be of type int() and 0 or greater") else: raise ValueError("lsb must be of type int() and 0 or greater") def __check_reg(reg_object): # Alowable struct.pack/unpack formats to check for fmts = { "b": 1, "B": 1, "h": 2, "H": 2, "f": 4, "i": 4, "I": 4, "l": 4, "L": 4, "q": 8, "Q": 8, } endians = "@><" byte_count = 0 # Take in only register objects if isinstance(reg_object, (RORegBit, RWRegBit, RORegBits, RWRegBits, ROReg, RWReg)): # Make sure they are strings if type(reg_object._fmt) == str and type(reg_object._endian) == str: # Check each letter in format string, To see if allowable for n in range(0, len(reg_object._fmt)): if reg_object._fmt[n] in fmts: # Add corresonding byte length to verify _num_bytes and format string agree byte_count = byte_count + fmts[reg_object._fmt[n]] else: raise ValueError( f"unsupported format code of '{reg_object._fmt[n]}'" ) if byte_count != reg_object._num_bytes: raise ValueError( f"format string accounts for {byte_count} bytes, _num_bytes value of {reg_object._num_bytes} does not match" ) else: raise TypeError("format and endian must be of type str()") else: raise TypeError( "incorrect object type - must be ROReg, RWReg, ROBits, RWBits, ROReg, RWReg" )