def __init__(
        self,
        name: str,
        bus: int,
        device_addr: int,
        mux_address: Optional[int],
        mux_channel: Optional[int],
        mux_simulator: Optional[MuxSimulator],
    ) -> None:

        # Initialize parameters
        self.name = name
        self.bus = bus
        self.device_addr = device_addr
        self.mux_address = mux_address
        self.mux_channel = mux_channel
        self.mux_simulator = mux_simulator

        # Initialize logger
        logname = "Simulator({})".format(name)
        self.logger = Logger(logname, __name__)
        self.logger.debug("Initializing simulator")

        # Initialize buffer
        self.buffer: bytearray = bytearray([])  # mutable bytes

        # Initialize register
        self.registers: Dict[int, int] = {}
        self.writes: Dict[str, bytes] = {}
    def __init__(self, name: str, bus: int) -> None:

        # Initialize parameters
        self.name = name
        self.bus = bus

        # Initialize logger
        logname = "DeviceIO({})".format(name)
        self.logger = Logger(logname, __name__)

        # Verify io exists
        self.logger.debug("Verifying io stream exists")
        self.open()
        self.close()
    def __init__(
        self,
        name: str,
        i2c_lock: threading.RLock,
        bus: int,
        address: int,
        mux: Optional[int] = None,
        channel: Optional[int] = None,
        mux_simulator: Optional[MuxSimulator] = None,
        PeripheralSimulator: Optional[PeripheralSimulator] = None,
        verify_device: bool = True,
    ) -> None:

        # Initialize passed in parameters
        self.name = name
        self.i2c_lock = i2c_lock
        self.bus = bus
        self.address = address
        self.mux = mux
        self.channel = channel

        # Initialize logger
        logname = "I2C({})".format(self.name)
        self.logger = Logger(logname, "i2c")
        self.logger.debug("Initializing communication")

        # Verify mux config
        if self.mux != None and self.channel == None:
            raise InitError(
                "Mux requires channel value to be set") from ValueError

        # Initialize io
        if PeripheralSimulator != None:
            self.logger.debug("Using simulated io stream")
            self.io = PeripheralSimulator(  # type: ignore
                name, bus, address, mux, channel, mux_simulator)
        else:
            self.logger.debug("Using device io stream")
            self.io = DeviceIO(name, bus)

        # Verify mux exists
        if self.mux != None:
            self.verify_mux()

        # Verify device exists
        if verify_device:
            self.verify_device()

        # Successfully initialized!
        self.logger.debug("Initialization successful")
class MuxSimulator(object):
    """I2C mux simulator. Note connections is a dict because we could have multiple
    muxes on a device."""

    # Initialize mux parameters
    valid_channel_bytes = [0x00, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80]
    connections: Dict[int, int] = {}

    def __init__(self) -> None:
        """Initializes mux simulator."""

        # Initialize logger
        self.logger = Logger("Simulator(Mux)", __name__)
        self.logger.debug("Initializing simulator")

    def set(self, address: int, channel_byte: int) -> None:
        """Sets mux at address to channel."""
        message = "Setting addr 0x{:02X} to 0x{:02X}".format(address, channel_byte)
        self.logger.debug(message)

        # Verify valid channel byte:
        if channel_byte not in self.valid_channel_bytes:
            message = "Unable to set mux, invalid channel byte: 0x{:02X}".format(
                channel_byte
            )
            raise MuxError(message)

        # Set mux to channel
        self.connections[address] = channel_byte

    def verify(self, address: int, channel: int) -> None:
        """Verifies if mux at address is set to correct channel."""
        self.logger.debug("Verifying mux connection")

        # Check mux exists
        if address not in self.connections:
            message = "Mux 0x{:02X} has never been set".format(address)
            raise MuxError(message, logger=self.logger)

        # Convert channel to channel byte
        channel_byte = 0x01 << channel
        if self.connections[address] != channel_byte:
            message = "Mux channel mismatch, stored: 0x{:02X}, received: 0x{:02X}".format(
                self.connections[address], channel_byte
            )
            raise MuxError(message, logger=self.logger)
    def __init__(self) -> None:
        """Initializes mux simulator."""

        # Initialize logger
        self.logger = Logger("Simulator(Mux)", __name__)
        self.logger.debug("Initializing simulator")
class DeviceIO(object):
    """Manages byte-level device IO.

    Attributes:
        name -- name of device
        bus -- device i2c bus
    """
    def __init__(self, name: str, bus: int) -> None:

        # Initialize parameters
        self.name = name
        self.bus = bus

        # Initialize logger
        logname = "DeviceIO({})".format(name)
        self.logger = Logger(logname, __name__)

        # Verify io exists
        self.logger.debug("Verifying io stream exists")
        self.open()
        self.close()

    def __del__(self) -> None:
        """Clean up any resources used by the I2c instance."""
        self.close()

    def __enter__(self) -> object:
        """Context manager enter function."""
        return self

    def __exit__(self, exc_type: ET, exc_val: EV, exc_tb: EB) -> bool:
        """Context manager exit function, ensures resources are cleaned up."""
        self.close()
        return False  # Don't suppress exceptions

    def open(self) -> None:
        """Opens io stream."""
        try:
            device_name = "/dev/i2c-{}".format(self.bus)
            self.io = io.open(device_name, "r+b", buffering=0)
        except PermissionError as e:
            message = "Unable to open device io: {}".format(device_name)
            raise InitError(message, logger=self.logger) from e

    def close(self) -> None:
        """Closes io stream."""
        try:
            self.io.close()
        except:
            self.logger.exception("Unable to close")

    @manage_io
    def write(self, address: int, bytes_: bytes) -> None:
        """ Writes bytes to IO stream. """
        try:
            fcntl.ioctl(self.io, I2C_SLAVE, address)
            self.io.write(bytes_)
        except IOError as e:
            message = "Unable to write: {}".format(bytes_)
            raise WriteError(message) from e

    @manage_io
    def read(self, address: int, num_bytes: int) -> bytes:
        """Reads bytes from io stream."""
        try:
            fcntl.ioctl(self.io, I2C_SLAVE, address)
            return bytes(self.io.read(num_bytes))
        except IOError as e:
            message = "Unable to read {} bytes".format(num_bytes)
            raise ReadError(message) from e

    @manage_io
    def read_register(self, address: int, register: int) -> int:
        """Reads register from io stream."""
        try:
            reg = c_uint8(register)
            result = c_uint8()
            request = make_i2c_rdwr_data(  # type: ignore
                [
                    (address, 0, 1, pointer(reg)),  # write cmd register
                    (address, I2C_M_RD, 1,
                     pointer(result)),  # read 1 byte as result
                ])
            fcntl.ioctl(self.io.fileno(), I2C_RDWR, request)
            byte_ = int(result.value)
            message = "Read register 0x{:02X}, value: 0x{:02X}".format(
                register, byte_)
            self.logger.debug(message)
            return byte_
        except IOError as e:
            message = "Unable to read register 0x{:02}".format(register)
            raise ReadError(message) from e

    @manage_io
    def write_register(self, address: int, register: int, value: int) -> None:
        """ Writes bytes to IO stream. """

        # Check register within range
        if register not in range(256):
            message = "Invalid register addrress: {}, must be 0-255".format(
                register)
            raise WriteError(message)

        # Check value within range
        if value not in range(256):
            message = "Invalid register value: {}, must be 0-255".format(value)
            raise WriteError(message)

        # Write to register
        self.write(address, bytes([register, value]))
class PeripheralSimulator:
    """I2C peripheral simulator base class."""

    def __init__(
        self,
        name: str,
        bus: int,
        device_addr: int,
        mux_address: Optional[int],
        mux_channel: Optional[int],
        mux_simulator: Optional[MuxSimulator],
    ) -> None:

        # Initialize parameters
        self.name = name
        self.bus = bus
        self.device_addr = device_addr
        self.mux_address = mux_address
        self.mux_channel = mux_channel
        self.mux_simulator = mux_simulator

        # Initialize logger
        logname = "Simulator({})".format(name)
        self.logger = Logger(logname, __name__)
        self.logger.debug("Initializing simulator")

        # Initialize buffer
        self.buffer: bytearray = bytearray([])  # mutable bytes

        # Initialize register
        self.registers: Dict[int, int] = {}
        self.writes: Dict[str, bytes] = {}

    def __enter__(self) -> object:
        """Context manager enter function."""
        return self

    def __exit__(self, exc_type: ET, exc_val: EV, exc_tb: EB) -> bool:
        """Context manager exit function, ensures resources are cleaned up."""
        return False  # Don't suppress exceptions

    @verify_mux
    def read(self, device_addr: int, num_bytes: int) -> bytes:
        """Reads bytes from buffer. Returns 0x00 if buffer is empty."""
        msg = "Reading {} bytes, buffer: {}".format(num_bytes, byte_str(self.buffer))
        self.logger.debug(msg)

        # Check device address matches
        if device_addr != self.device_addr:
            message = "Address not found: 0x{:02X}".format(device_addr)
            raise ReadError(message)

        # Pop bytes from buffer and return
        bytes_ = []
        while num_bytes > 0:

            # Check for empty buffer or pop byte from buffer
            if len(self.buffer) == 0:
                bytes_.append(0x00)
            else:
                bytes_.append(self.buffer.pop())

            # Decrement num bytes to read
            num_bytes = num_bytes - 1

        # Successfully read bytes
        return bytes(bytes_)

    def write(self, address: int, bytes_: bytes) -> None:
        """Writes bytes to buffer."""

        # Check if writing to mux
        if address == self.mux_address:

            # Check if mux command valid
            if len(bytes_) > 1:
                raise MuxError("Unable to set mux, only 1 command byte is allowed")

            # Set mux to channel
            self.mux_simulator.set(self.mux_address, bytes_[0])  # type: ignore

        # Check if writing to device
        elif address == self.device_addr:

            # Verify mux connection
            if self.mux_address != None:
                address = self.mux_address  # type: ignore
                channel = self.mux_channel
                self.mux_simulator.verify(address, channel)  # type: ignore

            # Get response bytes
            response_bytes = self.writes.get(byte_str(bytes_), None)

            # Verify known write bytes
            if response_bytes == None:
                raise WriteError("Unknown write bytes: {}".format(byte_str(bytes_)))

            # Write response bytes to buffer
            response_byte_string = byte_str(response_bytes)  # type: ignore
            self.logger.debug("Response bytes: {}".format(response_byte_string))
            for byte in response_bytes:  # type: ignore
                self.buffer.insert(0, byte)
            self.logger.debug("Buffer: {}".format(byte_str(self.buffer)))

        # Check for invalid address
        else:
            message = "Address not found: 0x{:02X}".format(address)
            raise WriteError(message)

    @verify_mux
    def read_register(self, device_addr: int, register_addr: int) -> int:
        """Reads register byte."""

        # Check address matches
        if device_addr != self.device_addr:
            message = "Address not found: 0x{:02X}".format(device_addr)
            raise ReadError(message)

        # Check register within range
        if register_addr not in range(256):
            message = "Invalid register addrress: {}, must be 0-255".format(
                register_addr
            )
            raise ReadError(message)

        # Read register value from register dict
        try:
            return self.registers[register_addr]
        except KeyError:
            message = "Register address not found: 0x{:02X}".format(register_addr)
            raise ReadError(message)

    @verify_mux
    def write_register(self, device_addr: int, register_addr: int, value: int) -> None:
        """Writes byte to register."""

        # Check address matches
        if device_addr != self.device_addr:
            message = "Device address not found: 0x{:02X}".format(device_addr)
            raise WriteError(message)

        # Check register within range
        if register_addr not in range(256):
            message = "Invalid register addrress: {}, must be 0-255".format(
                register_addr
            )
            raise WriteError(message)

        # Check value within range
        if value not in range(256):
            message = "Invalid register value: {}, must be 0-255".format(value)
            raise WriteError(message)

        # Write value to register
        self.registers[register_addr] = value
class I2C(object):
    """I2C communication device. Can communicate with device directly or
    via an I2C mux."""
    def __init__(
        self,
        name: str,
        i2c_lock: threading.RLock,
        bus: int,
        address: int,
        mux: Optional[int] = None,
        channel: Optional[int] = None,
        mux_simulator: Optional[MuxSimulator] = None,
        PeripheralSimulator: Optional[PeripheralSimulator] = None,
        verify_device: bool = True,
    ) -> None:

        # Initialize passed in parameters
        self.name = name
        self.i2c_lock = i2c_lock
        self.bus = bus
        self.address = address
        self.mux = mux
        self.channel = channel

        # Initialize logger
        logname = "I2C({})".format(self.name)
        self.logger = Logger(logname, "i2c")
        self.logger.debug("Initializing communication")

        # Verify mux config
        if self.mux != None and self.channel == None:
            raise InitError(
                "Mux requires channel value to be set") from ValueError

        # Initialize io
        if PeripheralSimulator != None:
            self.logger.debug("Using simulated io stream")
            self.io = PeripheralSimulator(  # type: ignore
                name, bus, address, mux, channel, mux_simulator)
        else:
            self.logger.debug("Using device io stream")
            self.io = DeviceIO(name, bus)

        # Verify mux exists
        if self.mux != None:
            self.verify_mux()

        # Verify device exists
        if verify_device:
            self.verify_device()

        # Successfully initialized!
        self.logger.debug("Initialization successful")

    def verify_mux(self) -> None:
        """Verifies mux exists by trying to set it to a channel."""
        try:
            self.logger.debug("Verifying mux exists")
            byte = self.set_mux(self.mux, self.channel, retry=True)
        except MuxError as e:
            message = "Unable to verify mux exists"
            raise InitError(message, logger=self.logger) from e

    def verify_device(self) -> None:
        """Verifies device exists by trying to read a byte from it."""
        try:
            self.logger.debug("Verifying device exists")
            byte = self.read(1, retry=True)
        except ReadError as e:
            message = "Unable to verify device exists, read error"
            raise InitError(message, logger=self.logger) from e
        except MuxError as e:
            message = "Unable to verify device exists, mux error"
            raise InitError(message, logger=self.logger) from e

    @retry((WriteError, MuxError), tries=5, delay=0.2, backoff=3)
    def write(self,
              bytes_: bytes,
              retry: bool = True,
              disable_mux: bool = False) -> None:
        """Writes byte list to device. Converts byte list to byte array then
        sends bytes. Returns error message."""
        with self.i2c_lock:
            self.manage_mux("write bytes", disable_mux)
            self.logger.debug("Writing bytes: {}".format(byte_str(bytes_)))
            self.io.write(self.address, bytes_)

    @retry((ReadError, MuxError), tries=5, delay=0.2, backoff=3)
    def read(self,
             num_bytes: int,
             retry: bool = True,
             disable_mux: bool = False) -> bytes:
        """Reads num bytes from device. Returns byte array."""
        with self.i2c_lock:
            self.manage_mux("read bytes", disable_mux)
            self.logger.debug("Reading {} bytes".format(num_bytes))
            bytes_ = bytes(self.io.read(self.address, num_bytes))
            self.logger.debug("Read bytes: {}".format(byte_str(bytes_)))
            return bytes_

    @retry((ReadError, MuxError), tries=5, delay=0.2, backoff=3)
    def read_register(self,
                      register: int,
                      retry: bool = True,
                      disable_mux: bool = False) -> int:
        """Reads byte stored in register at address."""
        with self.i2c_lock:
            self.manage_mux("read register", disable_mux)
            self.logger.debug("Reading register: 0x{:02X}".format(register))
            return int(self.io.read_register(self.address, register))

    @retry((WriteError, MuxError), tries=5, delay=0.2, backoff=3)
    def write_register(self,
                       register: int,
                       value: int,
                       retry: bool = True,
                       disable_mux: bool = False) -> None:
        with self.i2c_lock:
            self.manage_mux("write register", disable_mux)
            message = "Writing register: 0x{:02X}, value: 0x{:02X}".format(
                register, value)
            self.logger.debug(message)
            self.io.write_register(self.address, register, value)

    @retry(MuxError, tries=5, delay=0.2, backoff=3)
    def set_mux(self, mux: int, channel: int, retry: bool = True) -> None:
        """Sets mux to channel"""
        with self.i2c_lock:
            channel_byte = 0x01 << channel
            self.logger.debug(
                "Setting mux 0x{:02X} to channel {}, writing: [0x{:02X}]".
                format(mux, channel, channel_byte))
            try:
                self.io.write(mux, bytes([channel_byte]))
            except WriteError as e:
                raise MuxError("Unable to set mux", logger=self.logger) from e

    def manage_mux(self, message: str, disable_mux: bool) -> None:
        """Sets mux if enabled."""
        if disable_mux:
            return
        elif self.mux != None:
            self.logger.debug("Managing mux to {}".format(message))
            self.set_mux(self.mux, self.channel, retry=False)