Example #1
0
class Arduino(object):

    def __init__(
        self,
        name: str,
        arduino_lock: threading.RLock,
    ) -> None:

        # Initialize passed in parameters
        self.name = name
        self.arduino_lock = arduino_lock

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

        self.io = DeviceIO(name)

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

    @retry(tries=5, delay=0.2, backoff=3)
    def write_output(
        self, pin: string, val: string, retry: bool = True
    ) -> None:
        """This tells the Arduino to set a pin to a value"""
        with self.arduino_lock:
            self.logger.debug("{}: write_output(pin={}, val={})".format(self.name, pin, val))
            self.io.write_output(pin, val)

    @retry(tries=5, delay=0.2, backoff=3)
    def read_register(
        self, address: string, register: string
    ) -> string:
        """This informs the Arduino to read an I2C chip, and reports the value back"""
        with self.arduino_lock:
            self.logger.debug("{}: write_output(address={}, register={})".format(self.name, address, register))
            return self.io.read_register(address, register)

    @retry(tries=5, delay=0.2, backoff=3)
    def write_register(
        self, address: string, register: string, message: string
    ) -> None:
        """Similar to `read_register`, but this writes a message"""
        with self.arduino_lock:
            self.logger.debug("{}: write_register(address={}, register={}, message={})".format(self.name, address, register, message))
            self.io.write_register(self.address, register, message)
Example #2
0
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)
Example #3
0
class DAC5578Driver:
    """Driver for DAC5578 digital to analog converter."""
    def __init__(
        self,
        name: str,
        i2c_lock: threading.RLock,
        bus: int,
        address: int,
        mux: Optional[int] = None,
        channel: Optional[int] = None,
        simulate: bool = False,
        mux_simulator: Optional[MuxSimulator] = None,
    ) -> None:
        """Initializes DAC5578."""

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

        # Check if simulating
        if simulate:
            self.logger.info("Simulating driver")
            Simulator = DAC5578Simulator
        else:
            Simulator = None

        # Initialize I2C
        try:
            self.i2c = I2C(
                name="DAC5578-{}".format(name),
                i2c_lock=i2c_lock,
                bus=bus,
                address=address,
                mux=mux,
                channel=channel,
                mux_simulator=mux_simulator,
                PeripheralSimulator=Simulator,
            )
        except I2CError as e:
            raise exceptions.InitError(logger=self.logger) from e

    def write_output(self,
                     channel: int,
                     percent: int,
                     retry: bool = True,
                     disable_mux: bool = False) -> None:
        """Sets output value to channel."""
        message = "Writing output on channel {} to: {:.02F}%".format(
            channel, percent)
        self.logger.debug(message)

        # Check valid channel range
        if channel < 0 or channel > 7:
            message = "channel out of range, must be within 0-7"
            raise exceptions.WriteOutputError(message=message,
                                              logger=self.logger)

        # Check valid value range
        if percent < 0 or percent > 100:
            message = "output percent out of range, must be within 0-100"
            raise exceptions.WriteOutputError(message=message,
                                              logger=self.logger)

        # Convert output percent to byte, ensure 100% is byte 255
        if percent == 100:
            byte = 255
        else:
            byte = int(percent * 2.55)

        # Send set output command to dac
        self.logger.debug("Writing to dac: ch={}, byte={}".format(
            channel, byte))
        try:
            self.i2c.write(bytes([0x30 + channel, byte, 0x00]),
                           disable_mux=disable_mux)
        except I2CError as e:
            raise exceptions.WriteOutputError(logger=self.logger) from e

    def write_outputs(self, outputs: dict, retry: bool = True) -> None:
        """Sets output channels to output percents. Only sets mux once. 
        Keeps thread locked since relies on mux not changing."""
        self.logger.debug("Writing outputs: {}".format(outputs))

        # Check output dict is not empty
        if len(outputs) < 1:
            message = "output dict must not be empty"
            raise exceptions.WriteOutputsError(message=message,
                                               logger=self.logger)

        if len(outputs) > 8:
            print("outputs len = {}".format(len(outputs)))
            message = "output dict must not contain more than 8 entries"
            raise exceptions.WriteOutputsError(message=message,
                                               logger=self.logger)

        # Run through each output
        for channel, percent in outputs.items():
            message = "Writing output for ch {}: {}%".format(channel, percent)
            self.logger.debug(message)
            try:
                self.write_output(channel, percent, retry=retry)
            except exceptions.WriteOutputError as e:
                raise exceptions.WriteOutputsError(logger=self.logger) from e

    def read_power_register(self,
                            retry: bool = True) -> Optional[Dict[int, bool]]:
        """Reads power register."""
        self.logger.debug("Reading power register")

        # Read register
        try:
            self.i2c.write([0x40], retry=retry)
            bytes_ = self.i2c.read(2, retry=retry)
        except I2CError as e:
            raise exceptions.ReadPowerRegisterError(logger=self.logger) from e

        # Parse response bytes
        msb = bytes_[0]
        lsb = bytes_[1]
        powered_channels = {
            0: not bool(bitwise.get_bit_from_byte(4, msb)),
            1: not bool(bitwise.get_bit_from_byte(3, msb)),
            2: not bool(bitwise.get_bit_from_byte(2, msb)),
            3: not bool(bitwise.get_bit_from_byte(1, msb)),
            4: not bool(bitwise.get_bit_from_byte(0, msb)),
            5: not bool(bitwise.get_bit_from_byte(7, lsb)),
            6: not bool(bitwise.get_bit_from_byte(6, lsb)),
            7: not bool(bitwise.get_bit_from_byte(5, lsb)),
        }
        return powered_channels

    def set_high(self,
                 channel: Optional[int] = None,
                 retry: bool = True) -> None:
        """Sets channel high, sets all channels high if no channel is specified."""
        if channel != None:
            self.logger.debug("Setting channel {} high".format(channel))
            try:
                self.write_output(channel, 100, retry=retry)  # type: ignore
            except exceptions.WriteOutputError as e:
                raise exceptions.SetHighError(logger=self.logger) from e
        else:
            self.logger.debug("Setting all channels high")
            outputs = {
                0: 100,
                1: 100,
                2: 100,
                3: 100,
                4: 100,
                5: 100,
                6: 100,
                7: 100
            }
            try:
                self.write_outputs(outputs, retry=retry)
            except exceptions.WriteOutputsError as e:
                raise exceptions.SetHighError(logger=self.logger) from e

    def set_low(self,
                channel: Optional[int] = None,
                retry: bool = True) -> None:
        """Sets channel low, sets all channels low if no channel is specified."""
        if channel != None:
            self.logger.debug("Setting channel {} low".format(channel))
            try:
                self.write_output(channel, 100, retry=retry)  # type: ignore
            except exceptions.WriteOutputError as e:
                raise exceptions.SetLowError(logger=self.logger) from e
        else:
            self.logger.debug("Setting all channels low")
            outputs = {0: 0, 1: 0, 2: 0, 3: 0, 4: 0, 5: 0, 6: 0, 7: 0}
            try:
                self.write_outputs(outputs, retry=retry)
            except exceptions.WriteOutputsError as e:
                raise exceptions.SetLowError(logger=self.logger) from e
class USBCameraDriver:
    """Driver for a usb camera."""
    def __init__(
        self,
        name: str,
        vendor_id: int,
        product_id: int,
        resolution: str,
        simulate: bool = False,
        usb_mux_comms: Optional[Dict[str, Any]] = None,
        usb_mux_channel: Optional[int] = None,
        i2c_lock: Optional[threading.Lock] = None,
        mux_simulator: Optional[MuxSimulator] = None,
    ) -> None:
        """Initializes USB camera camera."""

        # Initialize parameters
        self.name = name
        self.vendor_id = vendor_id
        self.product_id = product_id
        self.resolution = resolution
        self.simulate = simulate

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

        # Check if simulating
        if simulate:
            self.logger.info("Simulating driver")
            self.directory = "device/peripherals/modules/usb_camera/tests/images/"
        else:
            self.directory = IMAGE_DIR

        # Check directory exists else create it
        if not os.path.exists(self.directory):
            os.makedirs(self.directory)

        # Check if using usb mux
        if usb_mux_comms == None or usb_mux_channel == None:
            self.dac5578 = None
            return

        # Get optional i2c parameters
        mux = usb_mux_comms.get("mux", None)  # type: ignore
        if mux != None:
            mux = int(mux, 16)

        # Using usb mux, initialize driver
        try:
            self.dac5578 = DAC5578Driver(
                name=name,
                i2c_lock=i2c_lock,  # type: ignore
                bus=usb_mux_comms.get("bus", None),  # type: ignore
                address=int(usb_mux_comms.get("address", None),
                            16),  # type: ignore
                mux=mux,
                channel=usb_mux_comms.get("channel", None),  # type: ignore
                simulate=simulate,
                mux_simulator=mux_simulator,
            )
            self.usb_mux_channel = usb_mux_channel
        except I2CError as e:
            raise InitError(logger=self.logger) from e

    def list_cameras(self,
                     vendor_id: Optional[int] = None,
                     product_id: Optional[int] = None) -> List[str]:
        """Returns list of cameras that match the provided vendor id and 
        product id."""

        # List all cameras
        cameras = glob.glob("/dev/video*")

        # Check if filtering by product and vendor id
        if vendor_id == None and product_id == None:
            return cameras

        # Check for product and vendor id matches
        matches = []
        for camera in cameras:
            if usb_device_matches(camera, vendor_id, product_id):
                matches.append(camera)

        return matches

    def get_camera(self) -> str:
        """Gets camera path."""

        # Get camera paths that match vendor and product ID
        cameras = self.list_cameras(self.vendor_id, self.product_id)

        # Check only one active camera
        if len(cameras) < 1:
            message = "no active cameras"
            raise GetCameraError(message=message, logger=self.logger)
        elif len(cameras) > 1:
            message = "too many active cameras"
            raise GetCameraError(message=message, logger=self.logger)

        # Successfuly got one camera
        return cameras[0]

    def enable_camera(self, retry: bool = True) -> None:
        """Enables camera by setting dac output high."""
        self.logger.debug("Enabling camera")

        # Turn on usb mux channel
        try:
            self.dac5578.set_high(channel=self.usb_mux_channel,
                                  retry=retry)  # type: ignore
        except DriverError as e:
            raise EnableCameraError(logger=self.logger) from e

        # Wait for camera to initialize
        time.sleep(5)

    def disable_camera(self, retry: bool = True) -> None:
        """Disables camera by setting dac output low."""
        self.logger.debug("Disabling camera")

        # Turn off usb mux channel
        try:
            self.dac5578.set_low(channel=self.usb_mux_channel,
                                 retry=retry)  # type: ignore
        except DriverError as e:
            raise DisableCameraError(logger=self.logger) from e

        # Wait for camera to power down
        start_time = time.time()
        while True:  # 5 second timeout

            # Look for camera
            try:
                camera = self.get_camera()

                # Check if camera powered down
                if camera == None:
                    self.logger.debug("Camera powered down")
                    return

            # TODO: Handle specific exceptions
            except Exception as e:
                raise DisableCameraError(logger=self.logger) from e

            # Check for timeout
            if time.time() - start_time > 5:  # 5 second timeout
                message = "timed out"
                raise DisableCameraError(message=message, logger=self.logger)

            # Update every 100ms
            time.sleep(0.1)

    def capture(self, retry: bool = True) -> None:
        """Manages usb mux and captures an image."""

        # Check if not using usb mux
        if self.dac5578 == None:
            return self.capture_image()

        # TODO: Lock camera threads

        # Manage 'mux' and capture image
        try:
            self.enable_camera()
            self.capture_image()
            self.disable_camera()
        except DriverError as e:
            raise CaptureError(logger=self.logger) from e

        # TODO: Unlock camera threads

    def capture_image(self) -> None:
        """Captures an image."""

        # Name image according to ISO8601
        timestr = datetime.datetime.utcnow().strftime("%Y-%m-%d-T%H:%M:%SZ")
        filename = timestr + "_" + self.name + ".png"

        # Specify filepaths
        image_path = self.directory + filename
        base_path = "device/peripherals/modules/usb_camera"
        dummy_path = "{}/dummy.png".format(base_path)
        active_path = "{}/active.jpg".format(base_path)

        # Check if simulated
        if self.simulate:
            self.logger.info(
                "Simulating capture, saving simulation image to: {}".format(
                    image_path))
            command = "cp device/peripherals/modules/usb_camera/tests/simulation_image.png {}".format(
                image_path)
            os.system(command)
            return

        # Camera not simulated, get camera path
        try:
            camera = self.get_camera()
        except GetCameraError as e:
            raise CaptureImageError(logger=self.logger) from e

        # Capture image
        try:
            # Take 3 low res images to clear out buffer
            self.logger.debug("Capturing dummy images")
            command = "fswebcam -d {} -r '320x240' ".format(camera, dummy_path)
            for i in range(3):
                os.system(command)

            # Try taking up to 3 real images
            self.logger.debug("Capturing active image")
            command = "fswebcam -d {} -r {} --png 9 -F 10 --no-banner --save {}".format(
                camera, self.resolution, active_path)
            valid_image = False
            for i in range(3):
                os.system(command)
                size = os.path.getsize(active_path)

                # Check if image meets minimum size constraint
                # TODO: Check lighting conditions (if box is dark, images are small)
                if size > 160000:  # 160kB
                    valid_image = True
                    break

            # Check if active image is valid, if so copy to images/ directory
            if not valid_image:
                self.logger.warning("Unable to capture a valid image")
            else:
                self.logger.info(
                    "Captured image, saved to {}".format(image_path))
                os.rename(active_path, image_path)

        except Exception as e:
            raise CaptureImageError(logger=self.logger) from e
class DeviceIO(object):
    """Manages byte-level device IO."""

    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:
            if os.getenv("IS_I2C_ENABLED") == "true":
                device_name = "/dev/i2c-{}".format(self.bus)
                self.io = io.open(device_name, "r+b", buffering=0)
            elif os.getenv("IS_USB_I2C_ENABLED") == "true":
                self.io = I2cController()
                self.io.configure("ftdi://ftdi:232h/1")  # type: ignore
            else:
                message = "Platform does not support i2c communication"
                raise InitError(message)
        except (PermissionError, I2cIOError, I2cNackError) 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:
            if os.getenv("IS_I2C_ENABLED") == "true":
                self.io.close()
            elif os.getenv("IS_USB_I2C_ENABLED") == "true":
                self.io.terminate()  # type: ignore
            else:
                message = "Platform does not support i2c communication"
                raise InitError(message)
        except:
            self.logger.exception("Unable to close")

    @manage_io
    def write(self, address: int, bytes_: bytes) -> None:
        """ Writes bytes to IO stream. """
        try:
            if os.getenv("IS_I2C_ENABLED") == "true":
                fcntl.ioctl(self.io, I2C_SLAVE, address)
                self.io.write(bytes_)
            elif os.getenv("IS_USB_I2C_ENABLED") == "true":
                device = self.io.get_port(address)  # type: ignore
                device.write(bytes_)
            else:
                message = "Platform does not support i2c communication"
                raise WriteError(message)
        except (IOError, I2cIOError, I2cNackError) 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:
            if os.getenv("IS_I2C_ENABLED") == "true":
                fcntl.ioctl(self.io, I2C_SLAVE, address)
                return bytes(self.io.read(num_bytes))
            elif os.getenv("IS_USB_I2C_ENABLED") == "true":
                device = self.io.get_port(address)  # type: ignore
                bytes_ = device.read(readlen=num_bytes)
                return bytes(bytes_)
            else:
                message = "Platform does not support i2c communication"
                raise ReadError(message)
        except (IOError, I2cIOError, I2cNackError) 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:
            if os.getenv("IS_I2C_ENABLED") == "true":
                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_
            elif os.getenv("IS_USB_I2C_ENABLED") == "true":
                device = self.io.get_port(address)  # type: ignore
                byte_raw = device.read_from(register, readlen=1)
                byte = int(byte_raw[0])
                return byte
            else:
                message = "Platform does not support i2c communication"
                raise ReadError(message)
        except (IOError, I2cIOError, I2cNackError) 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
        try:
            if os.getenv("IS_I2C_ENABLED") == "true":
                self.write(address, bytes([register, value]))
            elif os.getenv("IS_USB_I2C_ENABLED") == "true":
                device = self.io.get_port(address)  # type: ignore
                device.write_to(register, [value])
            else:
                message = "Platform does not support i2c communication"
                raise WriteError(message)
        except (IOError, I2cIOError, I2cNackError) as e:
            message = "Unable to write register 0x{:02}".format(register)
            raise WriteError(message) from e
Example #6
0
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]))
Example #7
0
class ArduinoCommsDriver:
    def __init__(
        self,
        name: str,
        i2c_lock: threading.RLock,
    ) -> None:
        """Initializes ArduinoComms"""

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

        self.bus = 2
        self.address = 0x08

        self.name = name

        # Initialize I2C
        try:
            self.i2c = I2C(name="Arduino-{}".format(name),
                           i2c_lock=i2c_lock,
                           bus=self.bus,
                           address=self.address,
                           mux=None)
        except I2CError as e:
            raise exceptions.InitError(logger=self.logger) from e

    def write_output(self, value, pin=None, pins=None):
        if pin:
            self.i2c.write(bytes("*w_{}_{}^".format(pin, value), 'utf-8'))
            self.logger.debug("{} write_output: *w_{}_{}^".format(
                self.name, pin, value))
        elif pins:
            for p in pins:
                self.i2c.write(bytes("*w_{}_{}^".format(p, value), 'utf-8'))
                self.logger.debug("{} write_output: *w_{}_{}^".format(
                    self.name, p, value))
                time.sleep(.1)
        else:
            raise exceptions.WriteOutputError(logger=self.logger)
        # for pin in self.pins:
        #     self.logger.debug("{} write_output: *w_{}_{}^".format(self.name, pin, value))
        #     self.i2c.write(bytes("*w_{}_{}^".format(pin, value), 'utf-8'))

    # def read_register(self, register):
    #     self.i2c.write(bytes("*r_{}_{}^".format(self.address, register), 'utf-8'))
    #
    #     num_bytes = 1
    #     self.i2c.read(num_bytes)
    #
    # def write_register(self, register, message):
    #     self.i2c.write(bytes("*r_{}_{}_{}^".format(self.address, register, message), 'utf-8'))

    def write_outputs(self, output_values):
        for pin, value in output_values.items():
            self.write_output(value, pin=pin)
            time.sleep(.1)

    def set_high(self, pin=None, pins=None) -> None:
        if pin:
            self.write_output(255, pin=pin)
            self.logger.debug("Setting pin {} high".format(pin))
        elif pins:
            self.write_output(255, pins=pin)
            self.logger.debug("Setting pins {} high".format(pins))
            time.sleep(.1)
        else:
            raise exceptions.SetHighError(logger=self.logger)

        # try:
        #     self.write_output(1)  # type: ignore
        # except exceptions.WriteOutputError as e:
        #     raise exceptions.SetHighError(logger=self.logger) from e

    def set_low(self, pin=None, pins=None) -> None:
        if pin:
            self.write_output(0, pin=pin)
            self.logger.debug("Setting pins {} low".format(pin))
        elif pins:
            self.write_output(0, pin=pins)
            self.logger.debug("Setting pins {} low".format(pins))
            time.sleep(.1)
        else:
            raise exceptions.SetLowError(logger=self.logger)
Example #8
0
class CCS811Driver:
    """Driver for atlas ccs811 carbon dioxide and total volatile organic compounds sensor."""

    # Initialize variable properties
    min_co2 = 400.0  # ppm
    max_co2 = 8192.0  # ppm
    min_tvoc = 0.0  # ppb
    max_tvoc = 1187.0  # ppb

    def __init__(
        self,
        name: str,
        i2c_lock: threading.Lock,
        bus: int,
        address: int,
        mux: Optional[int] = None,
        channel: Optional[int] = None,
        simulate: bool = False,
        mux_simulator: Optional[MuxSimulator] = None,
    ) -> None:

        # Initialize simulation mode
        self.simulate = simulate

        # Initialize logger
        self.logger = Logger(name="Driver({})".format(name), dunder_name=__name__)
        self.logger.info("Initializing driver")

        # Check if simulating
        if simulate:
            self.logger.info("Simulating driver")
            Simulator = CCS811Simulator
        else:
            Simulator = None

        # Initialize I2C
        try:
            self.i2c = I2C(
                name=name,
                i2c_lock=i2c_lock,
                bus=bus,
                address=address,
                mux=mux,
                channel=channel,
                mux_simulator=mux_simulator,
                PeripheralSimulator=Simulator,
            )
        except I2CError as e:
            raise InitError(logger=self.logger) from e

    def setup(self, retry: bool = True) -> None:
        """Setups sensor."""
        try:
            self.reset(retry=retry)
            self.check_hardware_id(retry=retry)
            self.start_app(retry=retry)
            # self.check_for_errors(retry=retry)
            self.write_measurement_mode(1, False, False, retry=retry)

            # Wait 20 minutes for sensor to stabilize
            start_time = time.time()
            while time.time() - start_time < 1200:

                # Keep logs active
                self.logger.info("Warming up, waiting for 20 minutes")

                # Update every 30 seconds
                time.sleep(30)

                # Break out if simulating
                if self.simulate:
                    break

        except DriverError as e:
            raise SetupError(logger=self.logger) from e

    def start_app(self, retry: bool = True) -> None:
        """Starts app by writing a byte to the app start register."""
        self.logger.info("Starting app")
        try:
            self.i2c.write(bytes([0xF4]), retry=retry)
        except I2CError as e:
            raise StartAppError(logger=self.logger) from e

    def read_hardware_id(self, retry: bool = True) -> int:
        """Reads hardware ID from sensor."""
        self.logger.info("Reading hardware ID")
        try:
            return int(self.i2c.read_register(0x20, retry=retry))
        except I2CError as e:
            raise ReadRegisterError(message="hw id reg", logger=self.logger) from e

    def check_hardware_id(self, retry: bool = True) -> None:
        """Checks for valid id in hardware id register."""
        self.logger.info("Checking hardware ID")
        hardware_id = self.read_hardware_id(retry=retry)
        if hardware_id != 0x81:
            raise HardwareIDError(logger=self.logger)

    def read_status_register(self, retry: bool = True) -> StatusRegister:
        """Reads status of sensor."""
        self.logger.info("Reading status register")
        try:
            byte = self.i2c.read_register(0x00, retry=retry)
        except I2CError as e:
            raise ReadRegisterError(message="status reg", logger=self.logger) from e

        # Parse status register byte
        status_register = StatusRegister(
            firmware_mode=bitwise.get_bit_from_byte(7, byte),
            app_valid=bool(bitwise.get_bit_from_byte(4, byte)),
            data_ready=bool(bitwise.get_bit_from_byte(3, byte)),
            error=bool(bitwise.get_bit_from_byte(0, byte)),
        )
        self.logger.debug(status_register)
        return status_register

    def check_for_errors(self, retry: bool = True) -> None:
        """Checks for errors in status register."""
        self.logger.info("Checking for errors")
        status_register = self.read_status_register(retry=retry)
        if status_register.error:
            raise StatusError(message=status_register, logger=self.logger)

    def read_error_register(self, retry: bool = True) -> ErrorRegister:
        """Reads error register."""
        self.logger.info("Reading error register")
        try:
            byte = self.i2c.read_register(0x0E, retry=retry)
        except I2CError as e:
            raise ReadRegisterError(message="error reg", logger=self.logger) from e

        # Parse error register byte
        return ErrorRegister(
            write_register_invalid=bool(bitwise.get_bit_from_byte(0, byte)),
            read_register_invalid=bool(bitwise.get_bit_from_byte(1, byte)),
            measurement_mode_invalid=bool(bitwise.get_bit_from_byte(2, byte)),
            max_resistance=bool(bitwise.get_bit_from_byte(3, byte)),
            heater_fault=bool(bitwise.get_bit_from_byte(4, byte)),
            heater_supply=bool(bitwise.get_bit_from_byte(5, byte)),
        )

    def write_measurement_mode(
        self,
        drive_mode: int,
        enable_data_ready_interrupt: bool,
        enable_threshold_interrupt: bool,
        retry: bool = True,
    ) -> None:
        """Writes measurement mode to the sensor."""
        self.logger.debug("Writing measurement mode")

        # Initialize bits
        bits = {7: 0, 1: 0, 0: 0}

        # Set drive mode
        if drive_mode == 0:
            bits.update({6: 0, 5: 0, 4: 0})
        elif drive_mode == 1:
            bits.update({6: 0, 5: 0, 4: 1})
        elif drive_mode == 2:
            bits.update({6: 0, 5: 1, 4: 0})
        elif drive_mode == 3:
            bits.update({6: 0, 5: 1, 4: 1})
        elif drive_mode == 4:
            bits.update({6: 1, 5: 0, 4: 0})
        else:
            raise ValueError("Invalid drive mode")

        # Set data ready interrupt
        bits.update({3: int(enable_data_ready_interrupt)})

        # Set threshold interrupt
        bits.update({2: int(enable_data_ready_interrupt)})

        # Convert bits to byte
        sbits = {}
        for key in sorted(bits.keys(), reverse=True):
            sbits[key] = bits[key]
        self.logger.error("bits = {}".format(sbits))  # TODO: remove
        write_byte = bitwise.get_byte_from_bits(bits)
        self.logger.error("write_byte = 0x{:02X}".format(write_byte))  # TODO: remove

        # Write measurement mode to sensor
        try:
            self.i2c.write(bytes([0x01, write_byte]), retry=retry)
        except I2CError as e:
            raise WriteMeasurementModeError(logger=self.logger) from e

    def write_environment_data(
        self,
        temperature: Optional[float] = None,
        humidity: Optional[float] = None,
        retry: bool = True,
    ) -> None:
        """Writes compensation temperature and / or humidity to sensor."""
        self.logger.debug("Writing environment data")

        # Check valid environment values
        if temperature == None and humidity == None:
            raise ValueError("Temperature and/or humidity value required")

        # Calculate temperature bytes
        if temperature != None:
            t = temperature
            temp_msb, temp_lsb = bitwise.convert_base_1_512(t + 25)  # type: ignore
        else:
            temp_msb = 0x64
            temp_lsb = 0x00

        # Calculate humidity bytes
        if humidity != None:
            hum_msb, hum_lsb = bitwise.convert_base_1_512(humidity)
        else:
            hum_msb = 0x64
            hum_lsb = 0x00

        # Write environment data to sensor
        bytes_ = [0x05, hum_msb, hum_lsb, temp_msb, temp_lsb]
        try:
            self.i2c.write(bytes(bytes_), retry=retry)
        except I2CError as e:
            raise WriteEnvironmentDataError(logger=self.logger) from e

    def read_algorithm_data(
        self, retry: bool = True, reread: int = 5
    ) -> Tuple[float, float]:
        """Reads algorighm data from sensor hardware."""
        self.logger.debug("Reading co2/tvoc algorithm data")

        # Read status register
        try:
            status = self.read_status_register()
        except ReadRegisterError as e:
            raise ReadAlgorithmDataError(logger=self.logger) from e

        # Check if data is ready
        if not status.data_ready:
            if reread:
                self.logger.debug("Data not ready yet, re-reading in 1 second")
                time.sleep(1)
                self.read_algorithm_data(retry=retry, reread=reread - 1)
            else:
                message = "data not ready"
                raise ReadAlgorithmDataError(message=message, logger=self.logger)

        # Get algorithm data
        try:
            self.i2c.write(bytes([0x02]), retry=retry)
            bytes_ = self.i2c.read(4)
        except I2CError:
            raise ReadAlgorithmDataError(logger=self.logger) from e

        # Parse data bytes
        co2 = float(bytes_[0] * 255 + bytes_[1])
        tvoc = float(bytes_[2] * 255 + bytes_[3])

        # Verify co2 value within valid range
        if co2 > self.min_co2 and co2 < self.min_co2:
            message = "CO2 reading outside of valid range"
            raise ReadAlgorithmDataError(message=message, logger=self.logger)

        # Verify tvos within valid range
        if tvoc > self.min_tvoc and tvoc < self.min_tvoc:
            message = "TVOC reading outside of valid range"
            raise ReadAlgorithmDataError(message=message, logger=self.logger)

        # Successfully read sensor data!
        self.logger.debug("CO2: {} ppm".format(co2))
        self.logger.debug("TVOC: {} ppb".format(tvoc))
        return co2, tvoc

    def read_raw_data(self) -> None:
        """Reads raw data from sensor."""
        ...

    def read_ntc(self) -> None:
        """ Read value of NTC. Can be used to calculate temperature. """
        ...

    def reset(self, retry: bool = True) -> None:
        """Resets sensor and places into boot mode."""
        self.logger.debug("Resetting sensor")

        # Write reset bytes to sensor
        bytes_ = [0xFF, 0x11, 0xE5, 0x72, 0x8A]
        try:
            self.i2c.write(bytes(bytes_), retry=retry)
        except I2CError as e:
            raise ResetError(logger=self.logger) from e
Example #9
0
# Import standard python modules
import subprocess, os, re

# Import device utilities
from device.utilities.logger import Logger

from django.conf import settings

# Initialize file paths
# DEVICE_CONFIG_PATH = "data/config/device.txt"
# DATA_PATH = os.getenv("STORAGE_LOCATION", "data")
DEVICE_CONFIG_PATH = settings.DATA_PATH + "/config/device.txt"

# Initialize logger
logger = Logger("SystemUtility", "system")
logger.debug("Initializing utility")


def device_config_name() -> str:
    """Gets device config name from file."""
    logger.debug("Getting device config name")

    # Get device config name
    if os.path.exists(DEVICE_CONFIG_PATH):
        with open(DEVICE_CONFIG_PATH) as f:
            device_config_name = f.readline().strip()
    else:
        device_config_name = "unspecified"

    # Successfully got device config name
    logger.debug("Device config name: {}".format(device_config_name))
Example #10
0
class LED:
    # --------------------------------------------------------------------------
    # Constants for PCA9632
    PCA9632_I2C_ADDRESS = 0x62
    R_BYTE = 3
    G_BYTE = 4
    B_BYTE = 5

    # --------------------------------------------------------------------------
    def __init__(
        self,
        bus: int = 0,
        address: int = PCA9632_I2C_ADDRESS,
        mux: Optional[int] = None,
        channel: Optional[int] = None,
        simulate: bool = False,
        mux_simulator: Optional[MuxSimulator] = None,
    ) -> None:
        """Initializes Grove RGB LCD."""

        # Initialize logger
        self.logger = Logger('LED', __name__)

        # Check if simulating
        if simulate:
            self.logger.info("Simulating LED")

        # Initialize I2C
        try:
            self.i2c = I2C(
                name="LED",
                i2c_lock=threading.RLock(),
                bus=bus,
                address=address,
                mux=mux,
                channel=channel,
                mux_simulator=mux_simulator,
                PeripheralSimulator=None,
            )
        except I2CError as e:
            raise LEDError(logger=self.logger) from e

        # Initialize the LED
        try:
            self.init_data = [
                0x80, 0x80, 0x21, 0x00, 0x00, 0x00, 0x40, 0x80, 0x02, 0xEA
            ]

            # init and clear any LED values
            self.i2c.write(bytes(self.init_data))

        except I2CError as e:
            raise LEDError(logger=self.logger) from e

    # --------------------------------------------------------------------------
    # Turn off all LEDs
    def off(self):
        try:
            data = self.init_data
            data[self.R_BYTE] = 0x00
            data[self.G_BYTE] = 0x00
            data[self.B_BYTE] = 0x00
            self.i2c.write(bytes(data))

        except I2CError as e:
            raise LEDError(logger=self.logger) from e

    # --------------------------------------------------------------------------
    def set(self, R: int = 0x00, G: int = 0x00, B: int = 0x00) -> None:
        # validate the inputs are 0 <> 255
        if R < 0 or R > 255 or G < 0 or G > 255 or B < 0 or B > 255:
            self.logger.error("RGB values must be between 0 and 255")
            raise LEDError(logger=self.logger)

        message = "Setting LED: {:2X}, {:2X}, {:2X}".format(R, G, B)
        self.logger.debug(message)

        # Set the backlight RGB value
        try:
            data = self.init_data
            data[self.R_BYTE] = R
            data[self.G_BYTE] = G
            data[self.B_BYTE] = B
            self.i2c.write(bytes(data))
        except I2CError as e:
            raise LEDError(logger=self.logger) from e
Example #11
0
class RecipeManager(StateMachineManager):
    """Manages recipe state machine thread."""

    def __init__(self, state: State) -> None:
        """Initializes recipe manager."""

        # Initialize parent class
        super().__init__()

        # Initialize logger
        self.logger = Logger("Recipe", "recipe")

        # Initialize state
        self.state = state

        # Initialize state machine transitions
        self.transitions = {
            modes.INIT: [modes.NORECIPE, modes.ERROR],
            modes.NORECIPE: [modes.START, modes.ERROR],
            modes.START: [modes.QUEUED, modes.ERROR],
            modes.QUEUED: [modes.NORMAL, modes.STOP, modes.ERROR],
            modes.NORMAL: [modes.PAUSE, modes.STOP, modes.ERROR],
            modes.PAUSE: [modes.START, modes.ERROR],
            modes.STOP: [modes.NORECIPE, modes.ERROR],
            modes.ERROR: [modes.RESET],
            modes.RESET: [modes.INIT],
        }

        # Start state machine from init mode
        self.mode = modes.INIT

    @property
    def mode(self) -> str:
        """Gets mode value. Important to keep this local so all
        state transitions only occur from within thread."""
        return self._mode

    @mode.setter
    def mode(self, value: str) -> None:
        """Safely updates recipe mode in shared state."""
        self._mode = value
        with self.state.lock:
            self.state.recipe["mode"] = value

    @property
    def stored_mode(self) -> Optional[str]:
        """Gets the stored mode from shared state."""
        value = self.state.recipe.get("stored_mode")
        if value != None:
            return str(value)
        else:
            return None

    @stored_mode.setter
    def stored_mode(self, value: Optional[str]) -> None:
        """Safely updates stored mode in shared state."""
        with self.state.lock:
            self.state.recipe["stored_mode"] = value

    @property
    def recipe_uuid(self) -> Optional[str]:
        """Gets recipe uuid from shared state."""
        value = self.state.recipe.get("recipe_uuid")
        if value != None:
            return str(value)
        else:
            return None

    @recipe_uuid.setter
    def recipe_uuid(self, value: Optional[str]) -> None:
        """Safely updates recipe uuid in shared state."""
        with self.state.lock:
            self.state.recipe["recipe_uuid"] = value

    @property
    def recipe_name(self) -> Optional[str]:
        """Gets recipe name from shared state."""
        value = self.state.recipe.get("recipe_name")
        if value != None:
            return str(value)
        else:
            return None

    @recipe_name.setter
    def recipe_name(self, value: Optional[str]) -> None:
        """ afely updates recipe name in shared state."""
        with self.state.lock:
            self.state.recipe["recipe_name"] = value

    @property
    def is_active(self) -> bool:
        """Gets value."""
        return self.state.recipe.get("is_active", False)  # type: ignore

    @is_active.setter
    def is_active(self, value: bool) -> None:
        """Safely updates value in shared state."""
        with self.state.lock:
            self.state.recipe["is_active"] = value

    @property
    def current_timestamp_minutes(self) -> int:
        """ Get current timestamp in minutes. """
        return int(time.time() / 60)

    @property
    def start_timestamp_minutes(self) -> Optional[int]:
        """ Gets start timestamp minutes from shared state. """
        value = self.state.recipe.get("start_timestamp_minutes")
        if value != None:
            return int(value)  # type: ignore
        else:
            return None

    @start_timestamp_minutes.setter
    def start_timestamp_minutes(self, value: Optional[int]) -> None:
        """Generates start datestring then safely updates start timestamp 
        minutes and datestring in shared state."""

        # Define var type
        start_datestring: Optional[str]

        # Generate start datestring
        if value != None:
            val_int = int(value)  # type: ignore
            start_datestring = (
                datetime.datetime.fromtimestamp(val_int * 60).strftime(
                    "%Y-%m-%d %H:%M:%S"
                )
                + " UTC"
            )
        else:
            start_datestring = None

        # Update start timestamp minutes and datestring in shared state
        with self.state.lock:
            self.state.recipe["start_timestamp_minutes"] = value
            self.state.recipe["start_datestring"] = start_datestring

    @property
    def start_datestring(self) -> Optional[str]:
        """Gets start datestring value from shared state."""
        return self.state.recipe.get("start_datestring")  # type: ignore

    @property
    def duration_minutes(self) -> Optional[int]:
        """Gets recipe duration in minutes from shared state."""
        return self.state.recipe.get("duration_minutes")  # type: ignore

    @duration_minutes.setter
    def duration_minutes(self, value: Optional[int]) -> None:
        """Generates duration string then safely updates duration string 
        and minutes in shared state. """

        # Define var type
        duration_string: Optional[str]

        # Generate duation string
        if value != None:
            duration_string = self.get_duration_string(value)  # type: ignore
        else:
            duration_string = None

        # Safely update duration minutes and string in shared state
        with self.state.lock:
            self.state.recipe["duration_minutes"] = value
            self.state.recipe["duration_string"] = duration_string

    @property
    def last_update_minute(self) -> Optional[int]:
        """Gets the last update minute from shared state."""
        return self.state.recipe.get("last_update_minute")  # type: ignore

    @last_update_minute.setter
    def last_update_minute(self, value: Optional[int]) -> None:
        """Generates percent complete, percent complete string, time
        remaining minutes, time remaining string, and time elapsed
        string then safely updates last update minute and aforementioned
        values in shared state. """

        # Define var types
        percent_complete_string: Optional[float]
        time_remaining_string: Optional[str]
        time_elapsed_string: Optional[str]

        # Generate values
        if value != None and self.duration_minutes != None:
            percent_complete = (
                float(value) / self.duration_minutes * 100  # type: ignore
            )
            percent_complete_string = "{0:.2f} %".format(  # type: ignore
                percent_complete
            )
            time_remaining_minutes = self.duration_minutes - value  # type: ignore
            time_remaining_string = self.get_duration_string(time_remaining_minutes)
            time_elapsed_string = self.get_duration_string(value)  # type: ignore
        else:
            percent_complete = None  # type: ignore
            percent_complete_string = None
            time_remaining_minutes = None
            time_remaining_string = None
            time_elapsed_string = None

        # Safely update values in shared state
        with self.state.lock:
            self.state.recipe["last_update_minute"] = value
            self.state.recipe["percent_complete"] = percent_complete
            self.state.recipe["percent_complete_string"] = percent_complete_string
            self.state.recipe["time_remaining_minutes"] = time_remaining_minutes
            self.state.recipe["time_remaining_string"] = time_remaining_string
            self.state.recipe["time_elapsed_string"] = time_elapsed_string

    @property
    def percent_complete(self) -> Optional[float]:
        """Gets percent complete from shared state."""
        return self.state.recipe.get("percent_complete")  # type: ignore

    @property
    def percent_complete_string(self) -> Optional[str]:
        """Gets percent complete string from shared state."""
        return self.state.recipe.get("percent_complete_string")  # type: ignore

    @property
    def time_remaining_minutes(self) -> Optional[int]:
        """Gets time remaining minutes from shared state."""
        return self.state.recipe.get("time_remaining_minutes")  # type: ignore

    @property
    def time_remaining_string(self) -> Optional[str]:
        """Gets time remaining string from shared state."""
        return self.state.recipe.get("time_remaining_string")  # type: ignore

    @property
    def time_elapsed_string(self) -> Optional[str]:
        """Gets time elapsed string from shared state."""
        value = self.state.recipe.get("time_elapsed_string")
        if value != None:
            return str(value)
        else:
            return None

    @property
    def current_phase(self) -> Optional[str]:
        """Gets the recipe current phase from shared state."""
        value = self.state.recipe.get("current_phase")
        if value != None:
            return str(value)
        else:
            return None

    @current_phase.setter
    def current_phase(self, value: str) -> None:
        """Safely updates current phase in shared state."""
        with self.state.lock:
            self.state.recipe["current_phase"] = value

    @property
    def current_cycle(self) -> Optional[str]:
        """Gets the current cycle from shared state."""
        value = self.state.recipe.get("current_cycle")
        if value != None:
            return str(value)
        else:
            return None

    @current_cycle.setter
    def current_cycle(self, value: Optional[str]) -> None:
        """Safely updates current cycle in shared state."""
        with self.state.lock:
            self.state.recipe["current_cycle"] = value

    @property
    def current_environment_name(self) -> Optional[str]:
        """Gets the current environment name from shared state"""
        value = self.state.recipe.get("current_environment_name")
        if value != None:
            return str(value)
        else:
            return None

    @current_environment_name.setter
    def current_environment_name(self, value: Optional[str]) -> None:
        """Safely updates current environment name in shared state."""
        with self.state.lock:
            self.state.recipe["current_environment_name"] = value

    @property
    def current_environment_state(self) -> Any:
        """Gets the current environment state from shared state."""
        return self.state.recipe.get("current_environment_name")

    @current_environment_state.setter
    def current_environment_state(self, value: Optional[Dict]) -> None:
        """ Safely updates current environment state in shared state. """
        with self.state.lock:
            self.state.recipe["current_environment_state"] = value
            self.set_desired_sensor_values(value)  # type: ignore

    ##### STATE MACHINE FUNCTIONS ######################################################

    def run(self) -> None:
        """Runs state machine."""

        # Loop forever
        while True:

            # Check if thread is shutdown
            if self.is_shutdown:
                break

            # Check for mode transitions
            if self.mode == modes.INIT:
                self.run_init_mode()
            if self.mode == modes.NORECIPE:
                self.run_norecipe_mode()
            elif self.mode == modes.START:
                self.run_start_mode()
            elif self.mode == modes.QUEUED:
                self.run_queued_mode()
            elif self.mode == modes.NORMAL:
                self.run_normal_mode()
            elif self.mode == modes.PAUSE:
                self.run_pause_mode()
            elif self.mode == modes.STOP:
                self.run_stop_mode()
            elif self.mode == modes.RESET:
                self.run_reset_mode()
            elif self.mode == modes.ERROR:
                self.run_error_mode()
            elif self.mode == modes.SHUTDOWN:
                self.run_shutdown_mode()
            else:
                self.logger.critical("Invalid state machine mode")
                self.mode = modes.INVALID
                self.is_shutdown = True
                break

    def run_init_mode(self) -> None:
        """Runs initialization mode. Checks for stored recipe mode and transitions to
        mode if exists, else transitions to no recipe mode."""
        self.logger.info("Entered INIT")

        # Initialize state
        self.is_active = False

        # Check for stored mode
        mode = self.state.recipe.get("stored_mode")
        if mode != None:
            self.logger.debug("Returning to stored mode: {}".format(mode))
            self.mode = mode  # type: ignore
        else:
            self.mode = modes.NORECIPE

    def run_norecipe_mode(self) -> None:
        """Runs no recipe mode. Clears recipe and desired sensor state then waits for
        new events and transitions."""
        self.logger.info("Entered NORECIPE")

        # Set run state
        self.is_active = False

        # Clear state
        self.clear_recipe_state()
        self.clear_desired_sensor_state()

        # Loop forever
        while True:

            # Check for events
            self.check_events()

            # Check for transitions
            if self.new_transition(modes.NORECIPE):
                break

            # Update every 100ms
            time.sleep(0.1)

    def run_start_mode(self) -> None:
        """Runs start mode. Loads commanded recipe uuid into shared state, 
        retrieves recipe json from recipe table, generates recipe 
        transitions, stores them in the recipe transitions table, extracts
        recipe duration and start time then transitions to queued mode."""

        # Set run state
        self.is_active = True

        try:
            self.logger.info("Entered START")

            # Get recipe json from recipe uuid
            recipe_json = models.RecipeModel.objects.get(uuid=self.recipe_uuid).json
            recipe_dict = json.loads(recipe_json)

            # Parse recipe transitions
            transitions = self.parse(recipe_dict)

            # Store recipe transitions in database
            self.store_recipe_transitions(transitions)

            # Set recipe duration
            self.duration_minutes = transitions[-1]["minute"]

            # Set recipe name
            self.recipe_name = recipe_dict["name"]
            self.logger.info("Started: {}".format(self.recipe_name))

            # Transition to queued mode
            self.mode = modes.QUEUED

        except Exception as e:
            message = "Unable to start recipe, unhandled exception {}".format(e)
            self.logger.critical(message)
            self.mode = modes.NORECIPE

    def run_queued_mode(self) -> None:
        """Runs queued mode. Waits for recipe start timestamp to be greater than
        or equal to current timestamp then transitions to NORMAL."""
        self.logger.info("Entered QUEUED")

        # Set state
        self.is_active = True

        # Initialize time counter
        prev_time_seconds = 0.0

        # Loop forever
        while True:

            # Check if recipe is ready to run
            current = self.current_timestamp_minutes
            start = self.start_timestamp_minutes
            if current >= start:  # type: ignore
                self.mode = modes.NORMAL
                break

            # Calculate remaining delay time
            delay_minutes = start - current  # type: ignore

            # Log remaining delay time every hour if remaining time > 1 hour
            if delay_minutes > 60 and time.time() > prev_time_seconds + 3600:
                prev_time_seconds = time.time()
                delay_hours = int(delay_minutes / 60.0)
                self.logger.debug("Starting recipe in {} hours".format(delay_hours))

            # Log remaining delay time every minute if remaining time < 1 hour
            elif delay_minutes < 60 and time.time() > prev_time_seconds + 60:
                prev_time_seconds = time.time()
                self.logger.debug("Starting recipe in {} minutes".format(delay_minutes))

            # Check for events
            self.check_events()

            # Check for transitions
            if self.new_transition(modes.QUEUED):
                break

            # Update every 100ms
            time.sleep(0.1)

    def run_normal_mode(self) -> None:
        """ Runs normal mode. Updates recipe and environment states every minute. 
        Checks for events and transitions."""
        self.logger.info("Entered NORMAL")

        # Set state
        self.is_active = True

        # Update recipe environment on first entry
        self.update_recipe_environment()

        # Loop forever
        while True:

            # Update recipe and environment states every minute
            if self.new_minute():
                self.update_recipe_environment()

            # Check for recipe end
            if self.current_phase == "End" and self.current_cycle == "End":
                self.logger.info("Recipe is over, so transitions from NORMAL to STOP")
                self.mode = modes.STOP
                break

            # Check for events
            self.check_events()

            # Check for transitions
            if self.new_transition(modes.NORMAL):
                break

            # Update every 100ms
            time.sleep(0.1)

    def run_pause_mode(self) -> None:
        """Runs pause mode. Clears recipe and desired sensor state, waits for new 
        events and transitions."""
        self.logger.info("Entered PAUSE")

        # Set state
        self.is_active = True

        # Clear recipe and desired sensor state
        self.clear_recipe_state()
        self.clear_desired_sensor_state()

        # Loop forever
        while True:

            # Check for events
            self.check_events()

            # Check for transitions
            if self.new_transition(modes.PAUSE):
                break

            # Update every 100ms
            time.sleep(0.1)

    def run_stop_mode(self) -> None:
        """Runs stop mode. Clears recipe and desired sensor state then transitions
        to no recipe mode."""
        self.logger.info("Entered STOP")

        # Clear recipe and desired sensor states
        self.clear_recipe_state()
        self.clear_desired_sensor_state()

        # Set state
        self.is_active = False

        # Transition to NORECIPE
        self.mode = modes.NORECIPE

    def run_error_mode(self) -> None:
        """Runs error mode. Clears recipe state and desired sensor state then waits 
        for new events and transitions."""
        self.logger.info("Entered ERROR")

        # Clear recipe and desired sensor states
        self.clear_recipe_state()
        self.clear_desired_sensor_state()

        # Set state
        self.is_active = False

        # Loop forever
        while True:

            # Check for events
            self.check_events()

            # Check for transitions
            if self.new_transition(modes.ERROR):
                break

            # Update every 100ms
            time.sleep(0.1)

    def run_reset_mode(self) -> None:
        """Runs reset mode. Clears error state then transitions to init mode."""
        self.logger.info("Entered RESET")

        # Transition to INIT
        self.mode = modes.INIT

    ##### HELPER FUNCTIONS #############################################################

    def get_recipe_environment(self, minute: int) -> Any:
        """Gets environment object from database for provided minute."""
        return (
            models.RecipeTransitionModel.objects.filter(minute__lte=minute).order_by(
                "-minute"
            ).first()
        )

    def store_recipe_transitions(self, recipe_transitions: List) -> None:
        """Stores recipe transitions in database."""

        # Clear recipe transitions table in database
        models.RecipeTransitionModel.objects.all().delete()

        # Create recipe transitions entries
        for transitions in recipe_transitions:
            models.RecipeTransitionModel.objects.create(
                minute=transitions["minute"],
                phase=transitions["phase"],
                cycle=transitions["cycle"],
                environment_name=transitions["environment_name"],
                environment_state=transitions["environment_state"],
            )

    def update_recipe_environment(self) -> None:
        """ Updates recipe environment. """
        self.logger.debug("Updating recipe environment")

        current = self.current_timestamp_minutes
        start = self.start_timestamp_minutes
        self.last_update_minute = current - start  # type: ignore
        environment = self.get_recipe_environment(self.last_update_minute)
        self.current_phase = environment.phase
        self.current_cycle = environment.cycle
        self.current_environment_name = environment.environment_name
        self.current_environment_state = environment.environment_state

    def clear_desired_sensor_state(self) -> None:
        """ Sets desired sensor state to null values. """
        with self.state.lock:
            for variable in self.state.environment["sensor"]["desired"]:
                self.state.environment["sensor"]["desired"][variable] = None

    def clear_recipe_state(self) -> None:
        """Sets recipe state to null values."""
        self.recipe_name = None
        self.recipe_uuid = None
        self.duration_minutes = None
        self.last_update_minute = None
        self.start_timestamp_minutes = None
        self.current_phase = None
        self.current_cycle = None
        self.current_environment_name = None
        self.current_environment_state = {}
        self.stored_mode = None

    def new_minute(self) -> bool:
        """Check if system clock is on a new minute."""
        current = self.current_timestamp_minutes
        start = self.start_timestamp_minutes
        current_minute = current - start  # type: ignore
        last_update_minute = self.state.recipe["last_update_minute"]
        if current_minute > last_update_minute:
            return True
        else:
            return False

    def get_duration_string(self, duration_minutes: int) -> str:
        """Converts duration in minutes to duration day-hour-minute string."""
        days = int(float(duration_minutes) / (60 * 24))
        hours = int((float(duration_minutes) - days * 60 * 24) / 60)
        minutes = int(duration_minutes - days * 60 * 24 - hours * 60)
        string = "{} Days {} Hours {} Minutes".format(days, hours, minutes)
        return string

    def set_desired_sensor_values(self, environment_dict: Dict) -> None:
        """Sets desired sensor values from provided environment dict."""
        with self.state.lock:
            for variable in environment_dict:
                value = environment_dict[variable]
                self.state.environment["sensor"]["desired"][variable] = value

    def validate(
        self, json_: str, should_exist: Optional[bool] = None
    ) -> Tuple[bool, Optional[str]]:
        """Validates a recipe. Returns true if valid."""

        # Load recipe schema
        schema = json.load(open(RECIPE_SCHEMA_PATH))

        # Check valid json and try to parse recipe
        try:
            # Decode json
            recipe = json.loads(json_)

            # Validate recipe against schema
            jsonschema.validate(recipe, schema)

            # Get top level recipe parameters
            format_ = recipe["format"]
            version = recipe["version"]
            name = recipe["name"]
            uuid = recipe["uuid"]
            cultivars = recipe["cultivars"]
            cultivation_methods = recipe["cultivation_methods"]
            environments = recipe["environments"]
            phases = recipe["phases"]

        except json.decoder.JSONDecodeError as e:
            message = "Invalid recipe json encoding: {}".format(e)
            self.logger.debug(message)
            return False, message
        except jsonschema.exceptions.ValidationError as e:
            message = "Invalid recipe json schema: {}".format(e.message)
            self.logger.debug(message)
            return False, message
        except KeyError as e:
            self.logger.critical("Recipe schema did not ensure `{}` exists".format(e))
            message = "Invalid recipe json schema: `{}` is requred".format(e)
            return False, message
        except Exception as e:
            self.logger.critical("Invalid recipe, unhandled exception: {}".format(e))
            return False, "Unhandled exception: {}".format(type(e))

        # Check valid uuid
        if uuid == None or len(uuid) == 0:
            return False, "Invalid uuid"

        # Check recipe existance criteria, does not check if should_exist == None
        recipe_exists = models.RecipeModel.objects.filter(uuid=uuid).exists()
        if should_exist == True and not recipe_exists:
            return False, "UUID does not exist"
        elif should_exist == False and recipe_exists:
            return False, "UUID already exists"

        # Check cycle environment key names are valid
        try:
            for phase in phases:
                for cycle in phase["cycles"]:
                    cycle_name = cycle["name"]
                    environment_key = cycle["environment"]
                    if environment_key not in environments:
                        message = "Invalid environment key `{}` in cycle `{}`".format(
                            environment_key, cycle_name
                        )
                        self.logger.debug(message)
                        return False, message
        except KeyError as e:
            self.logger.critical("Recipe schema did not ensure `{}` exists".format(e))
            message = "Invalid recipe json schema: `{}` is requred".format(e)
            return False, message

        # Build list of environment variables
        env_vars: List[str] = []
        for env_key, env_dict in environments.items():
            for env_var, _ in env_dict.items():
                if env_var != "name" and env_var not in env_vars:
                    env_vars.append(env_var)

        # Check environment variables are valid sensor variables
        for env_var in env_vars:
            if not models.SensorVariableModel.objects.filter(key=env_var).exists():
                message = "Invalid recipe environment variable: `{}`".format(env_var)
                self.logger.debug(message)
                return False, message

        """
        TODO: Reinstate these checks once cloud system has support for enforcing
        uniqueness of cultivars and cultivation methods. While we are at it, my as 
        well do the same for variable types so can create "scientific" recipes from 
        the cloud UI and send complete recipes. Cloud system will need a way to manage
        recipes and recipe derivatives. R.e. populating cultivars table, might just 
        want to scrape seedsavers or leverage another existing organism database. 
        Probably also want to think about organismal groups (i.e. classifications).
        Classifications could the standard scientific (Kingdom , Phylum, etc.) or a more
        user-friendly group (e.g. Leafy Greens, Six-Week Grows, Exotic Plants, 
        Pre-Historic Plants, etc.)


        # Check cultivars are valid
        for cultivar in cultivars:
            cultivar_name = cultivar["name"]
            cultivar_uuid = cultivar["uuid"]
            if not models.CultivarModel.objects.filter(uuid=cultivar_uuid).exists():
                message = "Invalid recipe cultivar: `{}`".format(cultivar_name)
                self.logger.debug(message)
                return False, message

        # Check cultivation methods are valid
        for method in cultivation_methods:
            method_name = method["name"]
            method_uuid = method["uuid"]
            if not models.CultivationMethodModel.objects.filter(
                uuid=method_uuid
            ).exists():
                message = "Invalid recipe cultivation method: `{}`".format(method_name)
                self.logger.debug(message)
                return False, message

        """

        # Recipe is valid
        return True, None

    def parse(self, recipe: Dict[str, Any]) -> List[Dict[str, Any]]:
        """ Parses recipe into state transitions. """
        transitions = []
        minute_counter = 0
        for phase in recipe["phases"]:
            phase_name = phase["name"]
            for i in range(phase["repeat"]):
                for cycle in phase["cycles"]:
                    # Get environment object and cycle name
                    environment_key = cycle["environment"]
                    environment = recipe["environments"][environment_key]
                    cycle_name = cycle["name"]

                    # Get duration
                    if "duration_hours" in cycle:
                        duration_hours = cycle["duration_hours"]
                        duration_minutes = duration_hours * 60
                    elif "duration_minutes" in cycle:
                        duration_minutes = cycle["duration_minutes"]
                    else:
                        raise KeyError(
                            "Could not find 'duration_minutes' or 'duration_hours' in cycle"
                        )

                    # Make shallow copy of env so we can pop a property locally
                    environment_copy = dict(environment)
                    environment_name = environment_copy["name"]
                    del environment_copy["name"]
                    environment_state = environment_copy

                    # Write recipe transition to database
                    transitions.append(
                        {
                            "minute": minute_counter,
                            "phase": phase_name,
                            "cycle": cycle_name,
                            "environment_name": environment_name,
                            "environment_state": environment_state,
                        }
                    )

                    # Increment minute counter
                    minute_counter += duration_minutes

        # Set recipe end
        transitions.append(
            {
                "minute": minute_counter,
                "phase": "End",
                "cycle": "End",
                "environment_name": "End",
                "environment_state": {},
            }
        )

        # Return state transitions
        return transitions

    def check_events(self) -> None:
        """Checks for a new event. Only processes one event per call, even if there are 
        multiple in the queue. Events are processed first-in-first-out (FIFO)."""

        # Check for new events
        if self.event_queue.empty():
            return

        # Get request
        request = self.event_queue.get()
        self.logger.debug("Received new request: {}".format(request))

        # Get request parameters
        try:
            type_ = request["type"]
        except KeyError as e:
            message = "Invalid request parameters: {}".format(e)
            self.logger.exception(message)
            return

        # Execute request
        if type_ == events.START:
            self._start_recipe(request)
        elif type_ == events.STOP:
            self._stop_recipe()
        else:
            self.logger.error("Invalid event request type in queue: {}".format(type_))

    def start_recipe(
        self, uuid: str, timestamp: Optional[float] = None, check_mode: bool = True
    ) -> Tuple[str, int]:
        """Adds a start recipe event to event queue."""
        self.logger.debug("Adding start recipe event to event queue")
        self.logger.debug("Recipe UUID: {}, timestamp: {}".format(uuid, timestamp))

        # Check recipe uuid exists
        if not models.RecipeModel.objects.filter(uuid=uuid).exists():
            message = "Unable to start recipe, invalid uuid"
            return message, 400

        # Check timestamp is valid if provided
        if timestamp != None and timestamp < time.time():  # type: ignore
            message = "Unable to start recipe, timestamp must be in the future"
            return message, 400

        # Check valid mode transition if enabled
        if check_mode and not self.valid_transition(self.mode, modes.START):
            message = "Unable to start recipe from {} mode".format(self.mode)
            self.logger.debug(message)
            return message, 400

        # Add start recipe event request to event queue
        request = {"type": events.START, "uuid": uuid, "timestamp": timestamp}
        self.event_queue.put(request)

        # Successfully added recipe to event queue
        message = "Starting recipe"
        return message, 202

    def _start_recipe(self, request: Dict[str, Any]) -> None:
        """Starts a recipe. Assumes request has been verified in public
        start recipe function."""
        self.logger.debug("Starting recipe")

        # Get request parameters
        uuid = request.get("uuid")
        timestamp = request.get("timestamp")

        # Convert timestamp to minutes if not None
        if timestamp != None:
            timestamp_minutes = int(timestamp / 60.0)  # type: ignore
        else:
            timestamp_minutes = int(time.time() / 60.0)

        # Check valid mode transition
        if not self.valid_transition(self.mode, modes.START):
            self.logger.critical("Tried to start recipe from {} mode".format(self.mode))
            return

        # Start recipe on next state machine update
        self.recipe_uuid = uuid
        self.start_timestamp_minutes = timestamp_minutes
        self.mode = modes.START

    def stop_recipe(self, check_mode: bool = True) -> Tuple[str, int]:
        """Adds stop recipe event to event queue."""
        self.logger.debug("Adding stop recipe event to event queue")

        # Check valid mode transition if enabled
        if check_mode and not self.valid_transition(self.mode, modes.STOP):
            message = "Unable to stop recipe from {} mode".format(self.mode)
            self.logger.debug(message)
            return message, 400

        # Put request into queue
        request = {"type": events.STOP}
        self.event_queue.put(request)

        # Successfully added stop recipe to event queue
        message = "Stopping recipe"
        return message, 200

    def _stop_recipe(self) -> None:
        """Stops a recipe. Assumes request has been verified in public
        stop recipe function."""
        self.logger.debug("Stopping recipe")

        # Check valid mode transition
        if not self.valid_transition(self.mode, modes.STOP):
            self.logger.critical("Tried to stop recipe from {} mode".format(self.mode))
            return

        # Stop recipe on next state machine update
        self.mode = modes.STOP

    def create_recipe(self, json_: str) -> Tuple[str, int]:
        """Creates a recipe into database."""
        self.logger.debug("Creating recipe")

        # Check if recipe is valid
        is_valid, error = self.validate(json_, should_exist=False)
        if not is_valid:
            message = "Unable to create recipe. {}".format(error)
            self.logger.debug(message)
            return message, 400

        # Create recipe in database
        try:
            recipe = json.loads(json_)
            models.RecipeModel.objects.create(json=json.dumps(recipe))
            message = "Successfully created recipe"
            return message, 200
        except:
            message = "Unable to create recipe, unhandled exception"
            self.logger.exception(message)
            return message, 500

    def update_recipe(self, json_: str) -> Tuple[str, int]:
        """Updates an existing recipe in database."""

        # Check if recipe is valid
        is_valid, error = self.validate(json_, should_exist=False)
        if not is_valid:
            message = "Unable to update recipe. {}".format(error)
            self.logger.debug(message)
            return message, 400

        # Update recipe in database
        try:
            recipe = json.loads(json_)
            r = models.RecipeModel.objects.get(uuid=recipe["uuid"])
            r.json = json.dumps(recipe)
            r.save()
            message = "Successfully updated recipe"
            return message, 200
        except:
            message = "Unable to update recipe, unhandled exception"
            self.logger.exception(message)
            return message, 500

    def create_or_update_recipe(self, json_: str) -> Tuple[str, int]:
        """Creates or updates an existing recipe in database."""

        # Check if recipe is valid
        is_valid, error = self.validate(json_, should_exist=None)
        if not is_valid:
            message = "Unable to create/update recipe -> {}".format(error)
            return message, 400

        # Check if creating or updating recipe in database
        recipe = json.loads(json_)
        if not models.RecipeModel.objects.filter(uuid=recipe["uuid"]).exists():

            # Create recipe
            try:
                recipe = json.loads(json_)
                models.RecipeModel.objects.create(json=json.dumps(recipe))
                message = "Successfully created recipe"
                return message, 200
            except:
                message = "Unable to create recipe, unhandled exception"
                self.logger.exception(message)
                return message, 500
        else:

            # Update recipe
            try:
                r = models.RecipeModel.objects.get(uuid=recipe["uuid"])
                r.json = json.dumps(recipe)
                r.save()
                message = "Successfully updated recipe"
                return message, 200
            except:
                message = "Unable to update recipe, unhandled exception"
                self.logger.exception(message)
                return message, 500

    def recipe_exists(self, uuid: str) -> bool:
        """Checks if a recipe exists."""
        return models.RecipeModel.objects.filter(uuid=uuid).exists()
Example #12
0
class SHT25Driver:
    """Driver for sht25 temperature and humidity sensor."""

    # Initialize variable properties
    min_temperature = -40  # celcius
    max_temperature = 125  # celcius
    min_humidity = 0  # %RH
    max_humidity = 100  # %RH

    def __init__(
        self,
        name: str,
        i2c_lock: threading.Lock,
        bus: int,
        address: int,
        mux: Optional[int] = None,
        channel: Optional[int] = None,
        simulate: Optional[bool] = False,
        mux_simulator: Optional[MuxSimulator] = None,
    ) -> None:
        """Initializes driver."""

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

        # Check if simulating
        if simulate:
            self.logger.info("Simulating driver")
            Simulator = SHT25Simulator
        else:
            Simulator = None

        # Initialize I2C
        try:
            self.i2c = I2C(
                name=name,
                i2c_lock=i2c_lock,
                bus=bus,
                address=address,
                mux=mux,
                channel=channel,
                mux_simulator=mux_simulator,
                PeripheralSimulator=Simulator,
                verify_device=False,  # need to write before device responds to read
            )
            self.read_user_register(retry=True)

        except I2CError as e:
            raise InitError(logger=self.logger) from e

    def read_temperature(self, retry: bool = True) -> Optional[float]:
        """ Reads temperature value."""
        self.logger.debug("Reading temperature")

        # Send read temperature command (no-hold master)
        try:
            self.i2c.write(bytes([0xF3]), retry=retry)
        except I2CError as e:
            raise ReadTemperatureError(logger=self.logger) from e

        # Wait for sensor to process, see datasheet Table 7
        # SHT25 is 12-bit so max temperature processing time is 22ms
        time.sleep(0.22)

        # Read sensor data
        try:
            bytes_ = self.i2c.read(2, retry=retry)
        except I2CError as e:
            raise ReadTemperatureError(logger=self.logger) from e

        # Convert temperature data and set significant figures
        msb, lsb = bytes_
        raw = msb * 256 + lsb
        temperature = float(-46.85 + ((raw * 175.72) / 65536.0))
        temperature = float("{:.0f}".format(temperature))

        # Verify temperature value within valid range
        if temperature > self.min_temperature and temperature < self.min_temperature:
            self.logger.warning("Temperature outside of valid range")
            return None

        # Successfully read temperature
        self.logger.debug("Temperature: {} C".format(temperature))
        return temperature

    def read_humidity(self, retry: bool = True) -> Optional[float]:
        """Reads humidity value."""
        self.logger.debug("Reading humidity value from hardware")

        # Send read humidity command (no-hold master)
        try:
            self.i2c.write(bytes([0xF5]), retry=retry)
        except I2CError as e:
            raise ReadHumidityError(logger=self.logger) from e

        # Wait for sensor to process, see datasheet Table 7
        # SHT25 is 12-bit so max humidity processing time is 29ms
        time.sleep(0.29)

        # Read sensor
        try:
            bytes_ = self.i2c.read(2, retry=retry)  # Read sensor data
        except I2CError as e:
            raise ReadHumidityError(logger=self.logger) from e

        # Convert humidity data and set significant figures
        msb, lsb = bytes_
        raw = msb * 256 + lsb
        humidity = float(-6 + ((raw * 125.0) / 65536.0))
        humidity = float("{:.0f}".format(humidity))

        # Verify humidity value within valid range
        if humidity > self.min_humidity and humidity < self.min_humidity:
            self.logger.warning("Humidity outside of valid range")
            return None

        # Successfully read humidity
        self.logger.debug("Humidity: {} %".format(humidity))
        return humidity

    def read_user_register(self, retry: bool = True) -> UserRegister:
        """ Reads user register."""
        self.logger.debug("Reading user register")

        # Read register
        try:
            byte = self.i2c.read_register(0xE7, retry=retry)
        except I2CError as e:
            raise ReadUserRegisterError(logger=self.logger) from e

        # Parse register content
        resolution_msb = bitwise.get_bit_from_byte(bit=7, byte=byte)
        resolution_lsb = bitwise.get_bit_from_byte(bit=0, byte=byte)
        user_register = UserRegister(
            resolution=resolution_msb << 1 + resolution_lsb,
            end_of_battery=bool(bitwise.get_bit_from_byte(bit=6, byte=byte)),
            heater_enabled=bool(bitwise.get_bit_from_byte(bit=2, byte=byte)),
            reload_disabled=bool(bitwise.get_bit_from_byte(bit=1, byte=byte)),
        )

        # Successfully read user register
        self.logger.debug("User register: {}".format(user_register))
        return user_register

    def reset(self, retry: bool = True) -> None:
        """Initiates soft reset."""
        self.logger.info("Initiating soft reset")

        # Send reset command
        try:
            self.i2c.write(bytes([0xFE]), retry=retry)
        except I2CError as e:
            raise ResetError(logger=self.logger) from e
Example #13
0
class NetworkUtility(ABC):
    """Abstract class that defines shared and required methods for all concrete NetworkUtility classes"""

    def __init__(self) -> None:
        self.logger = Logger("NetworkUtility", "network")

    def is_connected(self) -> bool:
        """Shared method to determine if the device can reach the network"""
        try:
            urllib.request.urlopen("https://google.com")
            return True
        except urllib.error.URLError as e:  # type: ignore
            self.logger.debug("Network is not connected: {}".format(e))
            return False

    def get_ip_address(self) -> str:
        """Shared method to determine the device's IP address"""
        self.logger.debug("Getting IP Address")

        try:
            s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
            s.connect(("8.8.8.8", 80))
            ip = str(s.getsockname()[0])
            s.close()
            return ip
        except Exception as e:
            message = "Unable to get ip address, unhandled exception: {}".format(
                type(e)
            )
            self.logger.exception(message)
            return "Unknown"

    @abstractmethod
    def get_wifi_ssids(
        self, exclude_hidden: bool = True, exclude_beaglebones: bool = True
    ) -> List[Dict[str, str]]:
        """Abstract method to get wifi SSIDs for configuration"""
        self.logger.debug("Generic Network Util, can't get SSIDs")
        return []

    @abstractmethod
    def join_wifi(self, ssid: str, password: str) -> None:
        """Abstract method to join a wifi network with just a password"""
        self.logger.debug("join_wifi_advance not implemented")
        pass

    @abstractmethod
    def join_wifi_advanced(
        self,
        ssid_name: str,
        passphrase: str,
        hidden_ssid: str,
        security: str,
        eap: str,
        identity: str,
        phase2: str,
    ) -> None:
        """Abstract method to join a wifi network with advanced options"""
        self.logger.debug("join_wifi_advance not implemented")
        pass

    @abstractmethod
    def delete_wifis(self) -> None:
        """Abstract method to forget all known wifi connections"""
        self.logger.debug("delete_wifis not implemented")
        raise SystemError("System does not support deleteing WiFis")
Example #14
0
class AtlasDriver:
    """Parent class for atlas drivers."""
    def __init__(
        self,
        name: str,
        i2c_lock: threading.RLock,
        bus: int,
        address: int,
        mux: Optional[int] = None,
        channel: Optional[int] = None,
        simulate: bool = False,
        mux_simulator: Optional[MuxSimulator] = None,
        Simulator: Optional[PeripheralSimulator] = None,
    ) -> None:
        """ Initializes atlas driver. """

        # Initialize parameters
        self.simulate = simulate

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

        # Initialize I2C
        try:
            self.i2c = I2C(
                name=name,
                i2c_lock=i2c_lock,
                bus=bus,
                address=address,
                mux=mux,
                channel=channel,
                mux_simulator=mux_simulator,
                PeripheralSimulator=Simulator,
            )
        except I2CError as e:
            raise exceptions.InitError(logger=self.logger)

    def setup(self, retry: bool = True) -> None:
        """Setsup sensor."""
        self.logger.debug("Setting up sensor")
        try:
            self.enable_led()
            info = self.read_info()
            if info.firmware_version > 1.94:
                self.enable_protocol_lock()
        except Exception as e:
            raise exceptions.SetupError(logger=self.logger) from e

    def process_command(
        self,
        command_string: str,
        process_seconds: float,
        num_bytes: int = 31,
        retry: bool = True,
        read_response: bool = True,
    ) -> Optional[str]:
        """Sends command string to device, waits for processing seconds, then
        tries to read num response bytes with optional retry if device
        returns a `still processing` response code. Read retry is enabled 
        by default. Returns response string on success or raises exception 
        on error."""
        self.logger.debug("Processing command: {}".format(command_string))

        try:
            # Send command to device
            byte_array = bytearray(command_string + "\00", "utf8")
            self.i2c.write(bytes(byte_array), retry=retry)

            # Check if reading response
            if read_response:
                return self.read_response(process_seconds,
                                          num_bytes,
                                          retry=retry)

            # Otherwise return none
            return None

        except Exception as e:
            raise exceptions.ProcessCommandError(logger=self.logger) from e

    def read_response(self,
                      process_seconds: float,
                      num_bytes: int,
                      retry: bool = True) -> str:
        """Reads response from from device. Waits processing seconds then 
        tries to read num response bytes with optional retry. Returns 
        response string on success or raises exception on error."""

        # Give device time to process
        self.logger.debug("Waiting for {} seconds".format(process_seconds))
        time.sleep(process_seconds)

        # Read device dataSet
        try:
            self.logger.debug("Reading response")
            data = self.i2c.read(num_bytes)
        except Exception as e:
            raise exceptions.ReadResponseError(logger=self.logger) from e

        # Format response code
        response_code = int(data[0])

        # Check for invalid syntax
        if response_code == 2:
            message = "invalid command string syntax"
            raise exceptions.ReadResponseError(message=message,
                                               logger=self.logger)

        # Check if still processing
        elif response_code == 254:

            # Try to read one more time if retry enabled
            if retry == True:
                self.logger.debug("Sensor still processing, retrying read")
                return self.read_response(process_seconds,
                                          num_bytes,
                                          retry=False)
            else:
                message = "insufficient processing time"
                raise exceptions.ReadResponseError(message, logger=self.logger)

        # Check if device has no data to send
        elif response_code == 255:

            # Try to read one more time if retry enabled
            if retry == True:
                self.logger.warning(
                    "Sensor reported no data to read, retrying read")
                return self.read_response(process_seconds,
                                          num_bytes,
                                          retry=False)
            else:
                message = "insufficient processing time"
                raise exceptions.ReadResponseError(message=message,
                                                   logger=self.logger)

        # Invalid response code
        elif response_code != 1:
            message = "invalid response code"
            raise exceptions.ReadResponseError(message=message,
                                               logger=self.logger)

        # Successfully read response
        response_message = str(data[1:].decode("utf-8").strip("\x00"))
        self.logger.debug("Response:`{}`".format(response_message))
        return response_message

    def read_info(self, retry: bool = True) -> Info:
        """Read sensor info register containing sensor type and firmware version. e.g. EC, 2.0."""
        self.logger.debug("Reading info register")

        # Send command
        try:
            response = self.process_command("i",
                                            process_seconds=0.3,
                                            retry=retry)
        except Exception as e:
            raise exceptions.ReadInfoError(logger=self.logger) from e

        # Parse response
        _, sensor_type, firmware_version = response.split(",")  # type: ignore
        firmware_version = float(firmware_version)

        # Store firmware version
        self.firmware_version = firmware_version

        # Create info dataclass
        info = Info(sensor_type=sensor_type.lower(),
                    firmware_version=firmware_version)

        # Successfully read info
        self.logger.debug(str(info))
        return info

    def read_status(self, retry: bool = True) -> Status:
        """Reads status from device."""
        self.logger.debug("Reading status register")
        try:
            response = self.process_command("Status",
                                            process_seconds=0.3,
                                            retry=retry)
        except Exception as e:
            raise exceptions.ReadStatusError(logger=self.logger) from e

        # Parse response message
        command, code, voltage = response.split(",")  # type: ignore

        # Break out restart code
        if code == "P":
            prev_restart_reason = "Powered off"
            self.logger.debug("Device previous restart due to powered off")
        elif code == "S":
            prev_restart_reason = "Software reset"
            self.logger.debug("Device previous restart due to software reset")
        elif code == "B":
            prev_restart_reason = "Browned out"
            self.logger.critical("Device browned out on previous restart")
        elif code == "W":
            prev_restart_reason = "Watchdog"
            self.logger.debug("Device previous restart due to watchdog")
        elif code == "U":
            self.prev_restart_reason = "Unknown"
            self.logger.warning("Device previous restart due to unknown")

        # Build status data class
        status = Status(prev_restart_reason=prev_restart_reason,
                        voltage=float(voltage))

        # Successfully read status
        self.logger.debug(str(status))
        return status

    def enable_protocol_lock(self, retry: bool = True) -> None:
        """Enables protocol lock."""
        self.logger.debug("Enabling protocol lock")
        try:
            self.process_command("Plock,1", process_seconds=0.9, retry=retry)
        except Exception as e:
            raise exceptions.EnableProtocolLockError(logger=self.logger) from e

    def disable_protocol_lock(self, retry: bool = True) -> None:
        """Disables protocol lock."""
        self.logger.debug("Disabling protocol lock")
        try:
            self.process_command("Plock,0", process_seconds=0.9, retry=retry)
        except Exception as e:
            raise exceptions.DisableProtocolLockError(
                logger=self.logger) from e

    def enable_led(self, retry: bool = True) -> None:
        """Enables led."""
        self.logger.debug("Enabling led")
        try:
            self.process_command("L,1", process_seconds=1.8, retry=retry)
        except Exception as e:
            raise exceptions.EnableLEDError(logger=self.logger) from e

    def disable_led(self, retry: bool = True) -> None:
        """Disables led."""
        self.logger.debug("Disabling led")
        try:
            self.process_command("L,0", process_seconds=1.8, retry=retry)
        except Exception as e:
            raise exceptions.DisableLEDError(logger=self.logger) from e

    def enable_sleep_mode(self, retry: bool = True) -> None:
        """Enables sleep mode, sensor will wake up by sending any command to it."""
        self.logger.debug("Enabling sleep mode")

        # Send command
        try:
            self.process_command("Sleep",
                                 process_seconds=0.3,
                                 read_response=False,
                                 retry=retry)
        except Exception as e:
            raise exceptions.EnableSleepModeError(logger=self.logger) from e

    def set_compensation_temperature(self,
                                     temperature: float,
                                     retry: bool = True) -> None:
        """Sets compensation temperature."""
        self.logger.debug("Setting compensation temperature")
        try:
            command = "T,{}".format(temperature)
            self.process_command(command, process_seconds=0.3, retry=retry)
        except Exception as e:
            raise exceptions.SetCompensationTemperatureError(
                logger=self.logger) from e

    def calibrate_low(self, value: float, retry: bool = True) -> None:
        """Takes a low point calibration reading."""
        self.logger.debug("Taking low point calibration reading")
        try:
            command = "Cal,low,{}".format(value)
            self.process_command(command, process_seconds=0.9, retry=retry)
        except Exception as e:
            raise exceptions.TakeLowPointCalibrationError(
                logger=self.logger) from e

    def calibrate_mid(self, value: float, retry: bool = True) -> None:
        """Takes a mid point calibration reading."""
        self.logger.debug("Taking mid point calibration reading")
        try:
            command = "Cal,mid,{}".format(value)
            self.process_command(command, process_seconds=0.9, retry=retry)
        except Exception as e:
            raise exceptions.TakeMidPointCalibrationError(
                logger=self.logger) from e

    def calibrate_high(self, value: float, retry: bool = True) -> None:
        """Takes a high point calibration reading."""
        self.logger.debug("Taking high point calibration reading")
        try:
            command = "Cal,high,{}".format(value)
            self.process_command(command, process_seconds=0.9, retry=retry)
        except Exception as e:
            raise exceptions.TakeHighPointCalibrationError(
                logger=self.logger) from e

    def clear_calibrations(self, retry: bool = True) -> None:
        """Clears calibration readings."""
        self.logger.debug("Clearing calibration readings")
        try:
            self.process_command("Cal,clear", process_seconds=0.9, retry=retry)
        except Exception as e:
            raise exceptions.ClearCalibrationError(logger=self.logger) from e

    def factory_reset(self, retry: bool = True) -> None:
        """Resets sensor to factory config."""
        self.logger.debug("Performing factory reset")
        try:
            self.process_command("Factory",
                                 process_seconds=0.3,
                                 read_response=False,
                                 retry=retry)
        except Exception as e:
            raise exceptions.FactoryResetError(logger=self.logger) from e
class DeviceIO(object):
    """Manages byte-level device IO."""

    def __init__(self, name: str) -> None:

        # Initialize parameters
        self.name = name

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

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

        UART.setup("UART1")
        self.ser = serial.Serial(port = "/dev/ttyO1", baudrate=9600)

        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:
            self.ser.open()
        except Exception 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.ser.close()
        except:
            self.logger.exception("Unable to close")

    @manage_io
    def write_output(self, pin: int, val: string) -> None:
        """This tells the Arduino to set a pin to a value"""
        try:
            self.send_to_ardunio("*w_{}_{}^".format(pin, val))
        except Exception as e:
            message = "Unable to write {}: write_output(pin={}, val={})".format(self.name, pin, val))
            raise WriteError(message) from e

    @manage_io
    def read_register(
        self, address: string, register: string
    ) -> string:
        """This informs the Arduino to read an I2C chip, and reports the value back"""
        try:
            resp = self.request_to_ardunio("*r_{}_{}^".format(address, register))
            return resp
        except Exception as e:
            message = "Unable to write {}: write_output(pin={}, val={})".format(self.name, pin, val))
            raise WriteError(message) from e
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,
        address: int,
        bus: Optional[int] = None,
        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")
            with self.i2c_lock:
                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)
Example #17
0
class T6713Driver:
    """Driver for t6713 co2 sensor."""

    # Initialize co2 properties
    min_co2 = 10  # ppm
    max_co2 = 5000  # ppm
    warmup_timeout = 120  # seconds

    def __init__(
        self,
        name: str,
        i2c_lock: threading.Lock,
        bus: int,
        address: int,
        mux: Optional[int] = None,
        channel: Optional[int] = None,
        simulate: Optional[bool] = False,
        mux_simulator: Optional[MuxSimulator] = None,
    ) -> None:
        """Initializes t6713 driver."""

        # Initialize parameters
        self.simulate = simulate
        self.i2c_lock = i2c_lock

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

        # Check if simulating
        if simulate:
            self.logger.info("Simulating driver")
            Simulator = T6713Simulator
        else:
            Simulator = None

        # Initialize I2C
        try:
            self.i2c = I2C(
                name=name,
                i2c_lock=i2c_lock,
                bus=bus,
                address=address,
                mux=mux,
                channel=channel,
                mux_simulator=mux_simulator,
                PeripheralSimulator=Simulator,
            )

        except I2CError as e:
            raise InitError(logger=self.logger) from e

    def setup(self, retry: bool = True) -> None:
        """Sets up sensor."""

        # Set ABC logic state
        try:
            self.enable_abc_logic()
        except EnableABCLogicError as e:
            raise SetupError(logger=self.logger) from e

        # Wait at least 2 minutes for sensor to stabilize
        start_time = time.time()
        while time.time() - start_time < 120:

            # Keep logs active
            self.logger.info("Warming up, waiting for 2 minutes")

            # Update every few seconds
            time.sleep(3)

            # Break out if simulating
            if self.simulate:
                break

        # Wait for sensor to report exiting warm up mode
        start_time = time.time()
        while True:

            # Keep logs active
            self.logger.info("Warming up, waiting for status")

            # Read status
            try:
                status = self.read_status()
            except ReadStatusError as e:
                raise SetupError(logger=self.logger) from e

            # Check if sensor completed warm up mode
            if not status.warm_up_mode:
                self.logger.info("Warmup complete")
                break

            # Check if timed out
            if time.time() - start_time > self.warmup_timeout:
                raise SetupError("Warmup period timed out", logger=self.logger)

            # Update every 3 seconds
            time.sleep(3)

    def read_co2(self, retry: bool = True) -> Optional[float]:
        """Reads co2 value."""
        self.logger.debug("Reading co2")

        # Read co2 data, requires mux disable to read all x4 bytes
        try:
            with self.i2c_lock:
                self.i2c.write(bytes([0x04, 0x13, 0x8b, 0x00, 0x01]),
                               retry=retry)
                bytes_ = self.i2c.read(4, retry=retry, disable_mux=True)
        except I2CError as e:
            raise ReadCo2Error(logger=self.logger) from e

        # Convert co2 data and set significant figures
        _, _, msb, lsb = bytes_
        co2 = float(msb * 256 + lsb)
        co2 = round(co2, 0)

        # Verify co2 value within valid range
        if co2 > self.min_co2 and co2 < self.min_co2:
            self.logger.warning("Co2 outside of valid range")
            return None

        # Successfully read carbon dioxide
        self.logger.debug("Co2: {} ppm".format(co2))
        return co2

    def read_status(self, retry: bool = True) -> Status:
        """Reads status."""
        self.logger.debug("Reading status")

        # Read status data, requires mux diable to read all x4 bytes
        try:
            with self.i2c_lock:
                self.i2c.write(bytes([0x04, 0x13, 0x8a, 0x00, 0x01]),
                               retry=retry)
                bytes_ = self.i2c.read(4, retry=retry, disable_mux=True)
        except I2CError as e:
            raise ReadStatusError(logger=self.logger) from e

        # Parse status bytes
        _, _, status_msb, status_lsb = bytes_
        status = Status(
            error_condition=bool(bitwise.get_bit_from_byte(0, status_lsb)),
            flash_error=bool(bitwise.get_bit_from_byte(1, status_lsb)),
            calibration_error=bool(bitwise.get_bit_from_byte(2, status_lsb)),
            rs232=bool(bitwise.get_bit_from_byte(0, status_msb)),
            rs485=bool(bitwise.get_bit_from_byte(1, status_msb)),
            i2c=bool(bitwise.get_bit_from_byte(2, status_msb)),
            warm_up_mode=bool(bitwise.get_bit_from_byte(3, status_msb)),
            single_point_calibration=bool(
                bitwise.get_bit_from_byte(7, status_msb)),
        )

        # Successfully read status
        self.logger.debug("Status: {}".format(status))
        return status

    def enable_abc_logic(self, retry: bool = True) -> None:
        """Enables ABC logic."""
        self.logger.info("Enabling abc logic")
        try:
            self.i2c.write(bytes([0x05, 0x03, 0xEE, 0xFF, 0x00]), retry=retry)
        except I2CError as e:
            raise EnableABCLogicError(logger=self.logger) from e

    def disable_abc_logic(self, retry: bool = True) -> None:
        """Disables ABC logic."""
        self.logger.info("Disabling abc logic")
        try:
            self.i2c.write(bytes([0x05, 0x03, 0xEE, 0x00, 0x00]), retry=retry)
        except I2CError as e:
            raise DisableABCLogicError(logger=self.logger) from e

    def reset(self, retry: bool = True) -> None:
        """Initiates soft reset."""
        self.logger.info("Performing soft reset")
        try:
            self.i2c.write(bytes([0x05, 0x03, 0xE8, 0xFF, 0x00]), retry=retry)
        except I2CError as e:
            raise ResetError(logger=self.logger) from e
Example #18
0
class CoordinatorManager(StateMachineManager):
    """Manages device state machine thread that spawns child threads to run 
    recipes, read sensors, set actuators, manage control loops, sync data, 
    and manage external events."""

    # Initialize vars
    latest_publish_timestamp = 0.0
    peripherals: Dict[str, StateMachineManager] = {}
    controllers: Dict[str, StateMachineManager] = {}
    new_config: bool = False

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

        # Initialize parent class
        super().__init__()

        # Initialize logger
        self.logger = Logger("Coordinator", "coordinator")
        self.logger.debug("Initializing coordinator")

        # Initialize state
        self.state = State()

        # Initialize environment state dict, TODO: remove this
        self.state.environment = {
            "sensor": {
                "desired": {},
                "reported": {}
            },
            "actuator": {
                "desired": {},
                "reported": {}
            },
            "reported_sensor_stats": {
                "individual": {
                    "instantaneous": {},
                    "average": {}
                },
                "group": {
                    "instantaneous": {},
                    "average": {}
                },
            },
        }

        # Initialize recipe state dict, TODO: remove this
        self.state.recipe = {
            "recipe_uuid": None,
            "start_timestamp_minutes": None,
            "last_update_minute": None,
        }

        # Initialize managers
        self.recipe = RecipeManager(self.state)
        self.iot = IotManager(self.state, self.recipe)  # type: ignore
        self.recipe.set_iot(self.iot)
        self.resource = ResourceManager(self.state, self.iot)  # type: ignore
        self.network = NetworkManager(self.state)  # type: ignore
        self.upgrade = UpgradeManager(self.state)  # type: ignore

        # Initialize state machine transitions
        self.transitions = {
            modes.INIT: [modes.CONFIG, modes.ERROR, modes.SHUTDOWN],
            modes.CONFIG: [modes.SETUP, modes.ERROR, modes.SHUTDOWN],
            modes.SETUP: [modes.NORMAL, modes.ERROR, modes.SHUTDOWN],
            modes.NORMAL: [modes.LOAD, modes.ERROR, modes.SHUTDOWN],
            modes.LOAD: [modes.CONFIG, modes.ERROR, modes.SHUTDOWN],
            modes.RESET: [modes.INIT, modes.SHUTDOWN],
            modes.ERROR: [modes.RESET, modes.SHUTDOWN],
        }

        # Initialize state machine mode
        self.mode = modes.INIT

    @property
    def mode(self) -> str:
        """Gets mode."""
        return self._mode

    @mode.setter
    def mode(self, value: str) -> None:
        """Safely updates mode in state object."""
        self._mode = value
        with self.state.lock:
            self.state.device["mode"] = value

    @property
    def config_uuid(self) -> Optional[str]:
        """ Gets config uuid from shared state. """
        return self.state.device.get("config_uuid")  # type: ignore

    @config_uuid.setter
    def config_uuid(self, value: Optional[str]) -> None:
        """ Safely updates config uuid in state. """
        with self.state.lock:
            self.state.device["config_uuid"] = value

    @property
    def config_dict(self) -> Dict[str, Any]:
        """Gets config dict for config uuid in device config table."""
        if self.config_uuid == None:
            return {}
        config = models.DeviceConfigModel.objects.get(uuid=self.config_uuid)
        return json.loads(config.json)  # type: ignore

    @property
    def latest_environment_timestamp(self) -> float:
        """Gets latest environment timestamp from environment table."""
        if not models.EnvironmentModel.objects.all():
            return 0.0
        else:
            environment = models.EnvironmentModel.objects.latest()
            return float(environment.timestamp.timestamp())

    @property
    def manager_modes(self) -> Dict[str, str]:
        """Gets manager modes."""
        self.logger.debug("Getting manager modes")

        # Get known manager modes
        modes = {
            "Coordinator": self.mode,
            "Recipe": self.recipe.mode,
            "Network": self.network.mode,
            "IoT": self.iot.mode,
            "Resource": self.resource.mode,
        }

        # Get peripheral manager modes
        for peripheral_name, peripheral_manager in self.peripherals.items():
            modes[peripheral_name] = peripheral_manager.mode

        # Get controller manager modes
        for controller_name, controller_manager in self.controllers.items():
            modes[controller_name] = controller_manager.mode

        # Return modes
        self.logger.debug("Returning modes: {}".format(modes))
        return modes

    @property
    def manager_healths(self) -> Dict[str, str]:
        """Gets manager healths."""
        self.logger.debug("Getting manager healths")

        # Initialize healths
        healths = {}

        # Get peripheral manager modes
        for peripheral_name, peripheral_manager in self.peripherals.items():
            healths[
                peripheral_name] = peripheral_manager.health  # type: ignore

        # Return modes
        self.logger.debug("Returning healths: {}".format(healths))
        return healths

    ##### STATE MACHINE FUNCTIONS ######################################################

    def run(self) -> None:
        """Runs device state machine."""

        # Loop forever
        while True:

            # Check if thread is shutdown
            if self.is_shutdown:
                break

            # Check for transitions
            if self.mode == modes.INIT:
                self.run_init_mode()
            elif self.mode == modes.CONFIG:
                self.run_config_mode()
            elif self.mode == modes.SETUP:
                self.run_setup_mode()
            elif self.mode == modes.NORMAL:
                self.run_normal_mode()
            elif self.mode == modes.LOAD:
                self.run_load_mode()
            elif self.mode == modes.ERROR:
                self.run_error_mode()
            elif self.mode == modes.RESET:
                self.run_reset_mode()
            elif self.mode == modes.SHUTDOWN:
                self.run_shutdown_mode()
            else:
                self.logger.critical("Invalid state machine mode")
                self.mode = modes.INVALID
                self.is_shutdown = True
                break

    def run_init_mode(self) -> None:
        """Runs init mode. Loads local data files and stored database state 
        then transitions to config mode."""
        self.logger.info("Entered INIT")

        # Load local data files and stored db state
        self.load_local_data_files()
        self.load_database_stored_state()

        # Transition to config mode on next state machine update
        self.mode = modes.CONFIG

    def run_config_mode(self) -> None:
        """Runs configuration mode. If device config is not set, loads 'unspecified' 
        config then transitions to setup mode."""
        self.logger.info("Entered CONFIG")

        # Check device config specifier file exists in repo
        try:
            with open(DEVICE_CONFIG_PATH) as f:
                config_name = f.readline().strip()
        except:

            env_dev_type = os.getenv("OPEN_AG_DEVICE_TYPE")
            if env_dev_type is None:
                config_name = "unspecified"
                message = "Unable to read {}, using unspecified config".format(
                    DEVICE_CONFIG_PATH)
            else:
                config_name = env_dev_type
                message = "Unable to read {}, using {} config from env".format(
                    DEVICE_CONFIG_PATH, config_name)

            self.logger.warning(message)

            # Create the directories if needed
            os.makedirs(os.path.dirname(DEVICE_CONFIG_PATH), exist_ok=True)

            # Write `unspecified` to device.txt
            with open(DEVICE_CONFIG_PATH, "w") as f:
                f.write("{}\n".format(config_name))

        # Load device config
        self.logger.debug("Loading device config file: {}".format(config_name))
        device_config = json.load(
            open("data/devices/{}.json".format(config_name)))

        # Check if config uuid changed, if so, adjust state
        if self.config_uuid != device_config["uuid"]:
            with self.state.lock:
                self.state.peripherals = {}
                self.state.controllers = {}
                set_nested_dict_safely(
                    self.state.environment,
                    ["reported_sensor_stats"],
                    {},
                    self.state.lock,
                )
                set_nested_dict_safely(self.state.environment,
                                       ["sensor", "reported"], {},
                                       self.state.lock)
                self.config_uuid = device_config["uuid"]

        # Transition to setup mode on next state machine update
        self.mode = modes.SETUP

    def run_setup_mode(self) -> None:
        """Runs setup mode. Creates and spawns recipe, peripheral, and 
        controller threads, waits for all threads to initialize then 
        transitions to normal mode."""
        self.logger.info("Entered SETUP")
        config_uuid = self.state.device["config_uuid"]

        # Spawn managers
        if not self.new_config:
            self.recipe.spawn()
            self.iot.spawn()
            self.resource.spawn()
            self.network.spawn()
            self.upgrade.spawn()

        # Create and spawn peripherals
        self.logger.debug("Creating and spawning peripherals")
        self.create_peripherals()
        self.spawn_peripherals()

        # Create and spawn controllers
        self.create_controllers()
        self.spawn_controllers()

        # Wait for all threads to initialize
        while not self.all_managers_initialized():
            time.sleep(0.2)

        # Unset new config flag
        self.new_config = False

        # Transition to normal mode on next state machine update
        self.mode = modes.NORMAL

    def run_normal_mode(self) -> None:
        """Runs normal operation mode. Updates device state summary and stores device 
        state in database, checks for new events and transitions."""
        self.logger.info("Entered NORMAL")

        while True:
            # Overwrite system state in database every 100ms
            self.update_state()

            # Store environment state in every 10 minutes
            if time.time() - self.latest_environment_timestamp > 60 * 10:
                self.store_environment()

            # Check for events
            self.check_events()

            # Check for transitions
            if self.new_transition(modes.NORMAL):
                break

            # Update every 100ms
            time.sleep(0.1)

    def run_load_mode(self) -> None:
        """Runs load mode, shutsdown peripheral and controller threads then transitions 
        to config mode."""
        self.logger.info("Entered LOAD")

        # Shutdown peripherals and controllers
        self.shutdown_peripheral_threads()
        self.shutdown_controller_threads()

        # Initialize timeout parameters
        timeout = 10
        start_time = time.time()

        # Loop forever
        while True:

            # Check if peripherals and controllers are shutdown
            if self.all_peripherals_shutdown(
            ) and self.all_controllers_shutdown():
                self.logger.debug("All peripherals and controllers shutdown")
                break

            # Check for timeout
            if time.time() - start_time > timeout:
                self.logger.critical("Config threads did not shutdown")
                self.mode = modes.ERROR
                return

            # Update every 100ms
            time.sleep(0.1)

        # Set new config flag
        self.new_config = True

        # Transition to config mode on next state machine update
        self.mode = modes.CONFIG

    def run_reset_mode(self) -> None:
        """Runs reset mode. Shutsdown child threads then transitions to init."""
        self.logger.info("Entered RESET")

        # Shutdown managers
        self.shutdown_peripheral_threads()
        self.shutdown_controller_threads()
        self.recipe.shutdown()
        self.iot.shutdown()

        # Transition to init mode on next state machine update
        self.mode = modes.INIT

    def run_error_mode(self) -> None:
        """Runs error mode. Shutsdown child threads, waits for new events 
        and transitions."""
        self.logger.info("Entered ERROR")

        # Shutsdown peripheral and controller threads
        self.shutdown_peripheral_threads()
        self.shutdown_controller_threads()

        # Loop forever
        while True:

            # Check for events
            self.check_events()

            # Check for transitions
            if self.new_transition(modes.ERROR):
                break

            # Update every 100ms
            time.sleep(0.1)

    ##### SUPPORT FUNCTIONS ############################################################

    def update_state(self) -> None:
        """Updates stored state in database. If state does not exist, creates it."""

        # TODO: Move this to state manager

        if not models.StateModel.objects.filter(pk=1).exists():
            models.StateModel.objects.create(
                id=1,
                device=json.dumps(self.state.device),
                recipe=json.dumps(self.state.recipe),
                environment=json.dumps(self.state.environment),
                peripherals=json.dumps(self.state.peripherals),
                controllers=json.dumps(self.state.controllers),
                iot=json.dumps(self.state.iot),
                resource=json.dumps(self.state.resource),
                connect=json.dumps(self.state.network),
                upgrade=json.dumps(self.state.upgrade),
            )
        else:
            models.StateModel.objects.filter(pk=1).update(
                device=json.dumps(self.state.device),
                recipe=json.dumps(self.state.recipe),
                environment=json.dumps(self.state.environment),
                peripherals=json.dumps(self.state.peripherals),
                controllers=json.dumps(self.state.controllers),
                iot=json.dumps(self.state.iot),
                resource=json.dumps(self.state.resource),
                connect=json.dumps(self.state.network),  # TODO: migrate this
                upgrade=json.dumps(self.state.upgrade),
            )

    def load_local_data_files(self) -> None:
        """ Loads local data files. """
        self.logger.info("Loading local data files")

        # Load files with no verification dependencies first
        self.load_sensor_variables_file()
        self.load_actuator_variables_file()
        self.load_cultivars_file()
        self.load_cultivation_methods_file()

        # Load recipe files after sensor/actuator variables, cultivars, and
        # cultivation methods since verification depends on them
        self.load_recipe_files()

        # Load peripheral setup files after sensor/actuator variable since verification
        # depends on them
        self.load_peripheral_setup_files()

        # Load controller setup files after sensor/actuator variable since verification
        # depends on them
        self.load_controller_setup_files()

        # Load device config after peripheral setups since verification
        # depends on  them
        self.load_device_config_files()

    def load_sensor_variables_file(self) -> None:
        """ Loads sensor variables file into database after removing all 
            existing entries. """
        self.logger.debug("Loading sensor variables file")

        # Load sensor variables and schema
        sensor_variables = json.load(open(SENSOR_VARIABLES_PATH))
        sensor_variables_schema = json.load(open(SENSOR_VARIABLES_SCHEMA_PATH))

        # Validate sensor variables with schema
        jsonschema.validate(sensor_variables, sensor_variables_schema)

        # Delete sensor variables tables
        models.SensorVariableModel.objects.all().delete()

        # Create sensor variables table
        for sensor_variable in sensor_variables:
            models.SensorVariableModel.objects.create(
                json=json.dumps(sensor_variable))

    def load_actuator_variables_file(self) -> None:
        """ Loads actuator variables file into database after removing all 
            existing entries. """
        self.logger.debug("Loading actuator variables file")

        # Load actuator variables and schema
        actuator_variables = json.load(open(ACTUATOR_VARIABLES_PATH))
        actuator_variables_schema = json.load(
            open(ACTUATOR_VARIABLES_SCHEMA_PATH))

        # Validate actuator variables with schema
        jsonschema.validate(actuator_variables, actuator_variables_schema)

        # Delete actuator variables tables
        models.ActuatorVariableModel.objects.all().delete()

        # Create actuator variables table
        for actuator_variable in actuator_variables:
            models.ActuatorVariableModel.objects.create(
                json=json.dumps(actuator_variable))

    def load_cultivars_file(self) -> None:
        """ Loads cultivars file into database after removing all 
            existing entries."""
        self.logger.debug("Loading cultivars file")

        # Load cultivars and schema
        cultivars = json.load(open(CULTIVARS_PATH))
        cultivars_schema = json.load(open(CULTIVARS_SCHEMA_PATH))

        # Validate cultivars with schema
        jsonschema.validate(cultivars, cultivars_schema)

        # Delete cultivars tables
        models.CultivarModel.objects.all().delete()

        # Create cultivars table
        for cultivar in cultivars:
            models.CultivarModel.objects.create(json=json.dumps(cultivar))

    def load_cultivation_methods_file(self) -> None:
        """ Loads cultivation methods file into database after removing all 
            existing entries. """
        self.logger.debug("Loading cultivation methods file")

        # Load cultivation methods and schema
        cultivation_methods = json.load(open(CULTIVATION_METHODS_PATH))
        cultivation_methods_schema = json.load(
            open(CULTIVATION_METHODS_SCHEMA_PATH))

        # Validate cultivation methods with schema
        jsonschema.validate(cultivation_methods, cultivation_methods_schema)

        # Delete cultivation methods tables
        models.CultivationMethodModel.objects.all().delete()

        # Create cultivation methods table
        for cultivation_method in cultivation_methods:
            models.CultivationMethodModel.objects.create(
                json=json.dumps(cultivation_method))

    def load_recipe_files(self) -> None:
        """Loads recipe files into database via recipe manager create or update 
        function."""
        self.logger.debug("Loading recipe files")

        # Get recipes
        for filepath in glob.glob(RECIPES_PATH):
            self.logger.debug("Loading recipe file: {}".format(filepath))
            with open(filepath, "r") as f:
                json_ = f.read().replace("\n", "")
                message, code = self.recipe.create_or_update_recipe(json_)
                if code != 200:
                    filename = filepath.split("/")[-1]
                    error = "Unable to load {} -> {}".format(filename, message)
                    self.logger.error(error)

    def load_peripheral_setup_files(self) -> None:
        """Loads peripheral setup files from codebase into database by creating new 
        entries after deleting existing entries. Verification depends on sensor and 
        actuator variables."""
        self.logger.info("Loading peripheral setup files")

        # Get peripheral setups
        peripheral_setups = []
        for filepath in glob.glob(PERIPHERAL_SETUP_FILES_PATH):
            self.logger.debug(
                "Loading peripheral setup file: {}".format(filepath))
            peripheral_setups.append(json.load(open(filepath)))

        # Get get peripheral setup schema
        # TODO: Finish schema
        peripheral_setup_schema = json.load(open(PERIPHERAL_SETUP_SCHEMA_PATH))

        # Validate peripheral setups with schema
        for peripheral_setup in peripheral_setups:
            jsonschema.validate(peripheral_setup, peripheral_setup_schema)

        # Delete all peripheral setup entries from database
        models.PeripheralSetupModel.objects.all().delete()

        # TODO: Validate peripheral setup variables with database variables

        # Create peripheral setup entries in database
        for peripheral_setup in peripheral_setups:
            models.PeripheralSetupModel.objects.create(
                json=json.dumps(peripheral_setup))

    def load_controller_setup_files(self) -> None:
        """Loads controller setup files from codebase into database by creating new 
        entries after deleting existing entries. Verification depends on sensor and 
        actuator variables."""
        self.logger.info("Loading controller setup files")

        # Get controller setups
        controller_setups = []
        for filepath in glob.glob(CONTROLLER_SETUP_FILES_PATH):
            self.logger.debug(
                "Loading controller setup file: {}".format(filepath))
            controller_setups.append(json.load(open(filepath)))

        # Get get controller setup schema
        controller_setup_schema = json.load(open(CONTROLLER_SETUP_SCHEMA_PATH))

        # Validate peripheral setups with schema
        for controller_setup in controller_setups:
            jsonschema.validate(controller_setup, controller_setup_schema)

        # Delete all peripheral setup entries from database
        models.ControllerSetupModel.objects.all().delete()

        # TODO: Validate controller setup variables with database variables

        # Create peripheral setup entries in database
        for controller_setup in controller_setups:
            models.ControllerSetupModel.objects.create(
                json=json.dumps(controller_setup))

    def load_device_config_files(self) -> None:
        """Loads device config files from codebase into database by creating new entries 
        after deleting existing entries. Verification depends on peripheral setups. """
        self.logger.info("Loading device config files")

        # Get devices
        device_configs = []
        for filepath in glob.glob(DEVICE_CONFIG_FILES_PATH):
            self.logger.debug(
                "Loading device config file: {}".format(filepath))
            device_configs.append(json.load(open(filepath)))

        # Get get device config schema
        # TODO: Finish schema (see optional objects)
        device_config_schema = json.load(open(DEVICE_CONFIG_SCHEMA_PATH))

        # Validate device configs with schema
        for device_config in device_configs:
            jsonschema.validate(device_config, device_config_schema)

        # TODO: Validate device config with peripherals
        # TODO: Validate device config with varibles

        # Delete all device config entries from database
        models.DeviceConfigModel.objects.all().delete()

        # Create device config entry if new or update existing
        for device_config in device_configs:
            models.DeviceConfigModel.objects.create(
                json=json.dumps(device_config))

    def load_database_stored_state(self) -> None:
        """ Loads stored state from database if it exists. """
        self.logger.info("Loading database stored state")

        # Get stored state from database
        if not models.StateModel.objects.filter(pk=1).exists():
            self.logger.info("No stored state in database")
            self.config_uuid = None
            return
        stored_state = models.StateModel.objects.filter(pk=1).first()

        # Load device state
        stored_device_state = json.loads(stored_state.device)

        # Load recipe state
        stored_recipe_state = json.loads(stored_state.recipe)
        self.recipe.recipe_uuid = stored_recipe_state["recipe_uuid"]
        self.recipe.recipe_name = stored_recipe_state["recipe_name"]
        self.recipe.duration_minutes = stored_recipe_state["duration_minutes"]
        self.recipe.start_timestamp_minutes = stored_recipe_state[
            "start_timestamp_minutes"]
        self.recipe.last_update_minute = stored_recipe_state[
            "last_update_minute"]
        self.recipe.stored_mode = stored_recipe_state["mode"]

        # Load peripherals state
        stored_peripherals_state = json.loads(stored_state.peripherals)
        for peripheral_name in stored_peripherals_state:
            self.state.peripherals[peripheral_name] = {}
            if "stored" in stored_peripherals_state[peripheral_name]:
                stored = stored_peripherals_state[peripheral_name]["stored"]
                self.state.peripherals[peripheral_name]["stored"] = stored

        # Load controllers state
        stored_controllers_state = json.loads(stored_state.controllers)
        for controller_name in stored_controllers_state:
            self.state.controllers[controller_name] = {}
            if "stored" in stored_controllers_state[controller_name]:
                stored = stored_controllers_state[controller_name]["stored"]
                self.state.controllers[controller_name]["stored"] = stored

        # Load iot state
        stored_iot_state = json.loads(stored_state.iot)
        self.state.iot["stored"] = stored_iot_state.get("stored", {})

    def store_environment(self) -> None:
        """ Stores current environment state in environment table. """
        models.EnvironmentModel.objects.create(state=self.state.environment)

    def create_peripherals(self) -> None:
        """ Creates peripheral managers. """
        self.logger.info("Creating peripheral managers")

        # Verify peripherals are configured
        if self.config_dict.get("peripherals") == None:
            self.logger.info("No peripherals configured")
            return

        # Set var type
        mux_simulator: Optional[MuxSimulator]

        # Inintilize simulation parameters
        if os.environ.get("SIMULATE") == "true":
            simulate = True
            mux_simulator = MuxSimulator()
        else:
            simulate = False
            mux_simulator = None

        # Create thread locks
        i2c_lock = threading.RLock()

        # Create peripheral managers
        self.peripherals = {}
        peripheral_config_dicts = self.config_dict.get("peripherals", {})
        for peripheral_config_dict in peripheral_config_dicts:
            self.logger.debug("Creating {}".format(
                peripheral_config_dict["name"]))

            # Get peripheral setup dict
            peripheral_uuid = peripheral_config_dict["uuid"]
            peripheral_setup_dict = self.get_peripheral_setup_dict(
                peripheral_uuid)

            self.logger.debug("UUID {}".format(peripheral_uuid))

            # Verify valid peripheral config dict
            if peripheral_setup_dict == {}:
                self.logger.critical(
                    "Invalid peripheral uuid in device "
                    "config. Validator should have caught this.")
                continue

            # Get peripheral module and class name
            module_name = ("device.peripherals.modules." +
                           peripheral_setup_dict["module_name"])
            class_name = peripheral_setup_dict["class_name"]

            # Import peripheral library
            module_instance = __import__(module_name, fromlist=[class_name])
            class_instance = getattr(module_instance, class_name)

            # Create peripheral manager
            peripheral_name = peripheral_config_dict["name"]

            peripheral = class_instance(
                name=peripheral_name,
                state=self.state,
                config=peripheral_config_dict,
                simulate=simulate,
                i2c_lock=i2c_lock,
                mux_simulator=mux_simulator,
            )
            self.peripherals[peripheral_name] = peripheral

    def get_peripheral_setup_dict(self, uuid: str) -> Dict[str, Any]:
        """Gets peripheral setup dict for uuid in peripheral setup table."""
        if not models.PeripheralSetupModel.objects.filter(uuid=uuid).exists():
            return {}
        else:
            json_ = models.PeripheralSetupModel.objects.get(uuid=uuid).json
        peripheral_setup_dict = json.loads(json_)
        return peripheral_setup_dict  # type: ignore

    def get_controller_setup_dict(self, uuid: str) -> Dict[str, Any]:
        """Gets controller setup dict for uuid in peripheral setup table."""
        if not models.ControllerSetupModel.objects.filter(uuid=uuid).exists():
            return {}
        else:
            json_ = models.ControllerSetupModel.objects.get(uuid=uuid).json
        controller_setup_dict = json.loads(json_)
        return controller_setup_dict  # type: ignore

    def spawn_peripherals(self) -> None:
        """ Spawns peripherals. """
        if self.peripherals == {}:
            self.logger.info("No peripheral threads to spawn")
        else:
            self.logger.info("Spawning peripherals")
            for name, manager in self.peripherals.items():
                manager.spawn()

    def create_controllers(self) -> None:
        """ Creates controller managers. """
        self.logger.info("Creating controller managers")

        # Verify controllers are configured
        if self.config_dict.get("controllers") == None:
            self.logger.info("No controllers configured")
            return

        # Create controller managers
        self.controllers = {}
        controller_config_dicts = self.config_dict.get("controllers", {})
        for controller_config_dict in controller_config_dicts:
            self.logger.debug("Creating {}".format(
                controller_config_dict["name"]))

            # Get controller setup dict
            controller_uuid = controller_config_dict["uuid"]
            controller_setup_dict = self.get_controller_setup_dict(
                controller_uuid)

            # Verify valid controller config dict
            if controller_setup_dict == None:
                self.logger.critical(
                    "Invalid controller uuid in device "
                    "config. Validator should have caught this.")
                continue

            # Get controller module and class name
            module_name = ("device.controllers.modules." +
                           controller_setup_dict["module_name"])
            class_name = controller_setup_dict["class_name"]

            # Import controller library
            module_instance = __import__(module_name, fromlist=[class_name])
            class_instance = getattr(module_instance, class_name)

            # Create controller manager
            controller_name = controller_config_dict["name"]
            controller_manager = class_instance(controller_name, self.state,
                                                controller_config_dict)
            self.controllers[controller_name] = controller_manager

    def spawn_controllers(self) -> None:
        """ Spawns controllers. """
        if self.controllers == {}:
            self.logger.info("No controller threads to spawn")
        else:
            self.logger.info("Spawning controllers")
            for name, manager in self.controllers.items():
                self.logger.debug("Spawning {}".format(name))
                manager.spawn()

    def all_managers_initialized(self) -> bool:
        """Checks if all managers have initialized."""
        if self.recipe.mode == modes.INIT:
            return False
        elif not self.all_peripherals_initialized():
            return False
        elif not self.all_controllers_initialized():
            return False
        return True

    def all_peripherals_initialized(self) -> bool:
        """Checks if all peripherals have initialized."""
        for name, manager in self.peripherals.items():
            if manager.mode == modes.INIT:
                return False
        return True

    def all_controllers_initialized(self) -> bool:
        """Checks if all controllers have initialized."""
        for name, manager in self.controllers.items():
            if manager.mode == modes.INIT:
                return False
        return True

    def shutdown_peripheral_threads(self) -> None:
        """Shutsdown all peripheral threads."""
        for name, manager in self.peripherals.items():
            manager.shutdown()

    def shutdown_controller_threads(self) -> None:
        """Shutsdown all controller threads."""
        for name, manager in self.controllers.items():
            manager.shutdown()

    def all_peripherals_shutdown(self) -> bool:
        """Check if all peripherals are shutdown."""
        for name, manager in self.peripherals.items():
            if manager.thread.is_alive():
                return False
        return True

    def all_controllers_shutdown(self) -> bool:
        """Check if all controllers are shutdown."""
        for name, manager in self.controllers.items():
            if manager.thread.is_alive():
                return False
        return True

    ##### EVENT FUNCTIONS ##############################################################

    def check_events(self) -> None:
        """Checks for a new event. Only processes one event per call, even if there are
        multiple in the queue. Events are processed first-in-first-out (FIFO)."""

        # Check for new events
        if self.event_queue.empty():
            return

        # Get request
        request = self.event_queue.get()
        self.logger.debug("Received new request: {}".format(request))

        # Get request parameters
        try:
            type_ = request["type"]
        except KeyError as e:
            message = "Invalid request parameters: {}".format(e)
            self.logger.exception(message)
            return

        # Execute request
        if type_ == events.RESET:
            self._reset()  # Defined in parent class
        elif type_ == events.SHUTDOWN:
            self._shutdown()  # Defined in parent class
        elif type_ == events.LOAD_DEVICE_CONFIG:
            self._load_device_config(request)
        else:
            self.logger.error(
                "Invalid event request type in queue: {}".format(type_))

    def load_device_config(self, uuid: str) -> Tuple[str, int]:
        """Pre-processes load device config event request."""
        self.logger.debug("Pre-processing load device config request")

        # Get filename of corresponding uuid
        filename = None
        for filepath in glob.glob(DEVICE_CONFIG_FILES_PATH):
            self.logger.debug(filepath)
            device_config = json.load(open(filepath))
            if device_config["uuid"] == uuid:
                filename = filepath.split("/")[-1].replace(".json", "")

        # Verify valid config uuid
        if filename == None:
            message = "Invalid config uuid, corresponding filepath not found"
            self.logger.debug(message)
            return message, 400

        # Check valid mode transition if enabled
        if not self.valid_transition(self.mode, modes.LOAD):
            message = "Unable to load device config from {} mode".format(
                self.mode)
            self.logger.debug(message)
            return message, 400

        # Add load device config event request to event queue
        request = {"type": events.LOAD_DEVICE_CONFIG, "filename": filename}
        self.event_queue.put(request)

        # Successfully added load device config request to event queue
        message = "Loading config"
        return message, 200

    def _load_device_config(self, request: Dict[str, Any]) -> None:
        """Processes load device config event request."""
        self.logger.debug("Processing load device config request")

        # Get request parameters
        filename = request.get("filename")

        # Write config filename to device config path
        with open(DEVICE_CONFIG_PATH, "w") as f:
            f.write(str(filename) + "\n")

        # Transition to init mode on next state machine update
        self.mode = modes.LOAD
class GroveRGBLCDDriver:
    """Driver for Grove RGB LCD display."""

    # --------------------------------------------------------------------------
    # Constants for Grove RBG LCD
    CMD = 0x80
    CLEAR = 0x01
    DISPLAY_ON_NO_CURSOR = 0x08 | 0x04
    TWO_LINES = 0x28
    CHAR = 0x40
    NEWLINE = 0xC0
    RGB_ADDRESS = 0x62
    LCD_ADDRESS = 0x3E

    # --------------------------------------------------------------------------
    def __init__(
        self,
        name: str,
        i2c_lock: threading.RLock,
        bus: int,
        rgb_address: int = RGB_ADDRESS,
        lcd_address: int = LCD_ADDRESS,
        mux: Optional[int] = None,
        channel: Optional[int] = None,
        simulate: bool = False,
        mux_simulator: Optional[MuxSimulator] = None,
    ) -> None:
        """Initializes Grove RGB LCD."""

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

        # Check if simulating
        if simulate:
            self.logger.info("Simulating driver")
            Simulator = simulator.GroveRGBLCDSimulator
        else:
            Simulator = None

        # Initialize I2C
        try:
            self.i2c_rgb = I2C(
                name="RGB-{}".format(name),
                i2c_lock=i2c_lock,
                bus=bus,
                address=rgb_address,
                mux=mux,
                channel=channel,
                mux_simulator=mux_simulator,
                PeripheralSimulator=Simulator,
            )
            self.i2c_lcd = I2C(
                name="LCD-{}".format(name),
                i2c_lock=i2c_lock,
                bus=bus,
                address=lcd_address,
                mux=mux,
                channel=channel,
                mux_simulator=mux_simulator,
                PeripheralSimulator=Simulator,
            )
        except I2CError as e:
            raise exceptions.InitError(logger=self.logger) from e

        # Initialize the display
        try:
            # command: clear display
            self.i2c_lcd.write(bytes([self.CMD, self.CLEAR]))
            time.sleep(0.05)  # Wait for lcd to process

            # command: display on, no cursor
            self.i2c_lcd.write(bytes([self.CMD, self.DISPLAY_ON_NO_CURSOR]))

            # command: 2 lines
            self.i2c_lcd.write(bytes([self.CMD, self.TWO_LINES]))
            time.sleep(0.05)  # Wait for lcd to process

        except I2CError as e:
            raise exceptions.DriverError(logger=self.logger) from e

    # --------------------------------------------------------------------------
    def set_backlight(self,
                      R: int = 0x00,
                      G: int = 0x00,
                      B: int = 0x00) -> None:
        """Turns on the LCD backlight at the level and color specified. 0 - 255 are valid inputs for RGB."""
        # validate the inputs are 0 <> 255
        if R < 0 or R > 255 or G < 0 or G > 255 or B < 0 or B > 255:
            self.logger.error("RGB values must be between 0 and 255")
            raise exceptions.DriverError(logger=self.logger)

        message = "Setting RGB backlight: {:2X}, {:2X}, {:2X}".format(R, G, B)
        self.logger.debug(message)

        # Set the backlight RGB value
        try:
            self.i2c_rgb.write(bytes([0, 0]))
            self.i2c_rgb.write(bytes([1, 0]))
            self.i2c_rgb.write(bytes([0x08, 0xAA]))
            self.i2c_rgb.write(bytes([4, R]))
            self.i2c_rgb.write(bytes([3, G]))
            self.i2c_rgb.write(bytes([2, B]))
        except I2CError as e:
            raise exceptions.DriverError(logger=self.logger) from e

    # --------------------------------------------------------------------------
    def write_string(self, message: str = "") -> None:
        """Writes a string to the LCD (16 chars per line limit, 2 lines). Use a '/n' newline character in the string to start the secone line."""
        self.logger.debug("Writing '{}' to LCD".format(message))
        try:
            # command: clear display
            self.i2c_lcd.write(bytes([self.CMD, self.CLEAR]))
            time.sleep(0.05)  # Wait for lcd to process
            for char in message:
                # write to the second line? (two lines max, not enforced)
                if char == "\n":
                    self.i2c_lcd.write(bytes([self.CMD, self.NEWLINE]))
                    continue
                # get the hex value of the char
                c = ord(char)
                self.i2c_lcd.write(bytes([self.CHAR, c]))
                # (there is a 16 char per line limit that I'm not enforcing)
        except I2CError as e:
            raise exceptions.DriverError(logger=self.logger) from e

    # --------------------------------------------------------------------------
    def display_time(self, retry: bool = True) -> None:
        """Clears LCD and displays current time."""
        # utc = time.gmtime()
        lt = time.localtime()
        now = "{}".format(time.strftime("%F %X", lt))
        self.logger.debug("Writing time {}".format(now))
        try:
            # command: clear display
            self.i2c_lcd.write(bytes([self.CMD, self.CLEAR]))
            time.sleep(0.05)  # Wait for lcd to process
            self.write_string(now)
        except exceptions.DriverError as e:
            raise exceptions.DriverError(logger=self.logger) from e
Example #20
0
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
Example #21
0
class LEDDAC5578Driver:
    """Driver for array of led panels controlled by a dac5578."""

    # Initialize var defaults
    num_active_panels = 0
    num_expected_panels = 1

    def __init__(
        self,
        name: str,
        panel_configs: List[Dict[str, Any]],
        panel_properties: Dict[str, Any],
        i2c_lock: threading.Lock,
        simulate: bool = False,
        mux_simulator: Optional[MuxSimulator] = None,
    ) -> None:
        """Initializes driver."""

        # Initialize driver parameters
        self.panel_properties = panel_properties
        self.i2c_lock = i2c_lock
        self.simulate = simulate

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

        # Parse panel properties
        self.channels = self.panel_properties.get("channels")
        self.dac_map = self.panel_properties.get("dac_map")

        # Initialze num expected panels
        self.num_expected_panels = len(panel_configs)

        # Initialize panels
        self.panels: List[LEDDAC5578Panel] = []
        for config in panel_configs:
            panel = LEDDAC5578Panel(name, config, i2c_lock, simulate,
                                    mux_simulator, self.logger)
            panel.initialize()
            self.panels.append(panel)

        # Check at least one panel is still active
        active_panels = [
            panel for panel in self.panels if not panel.is_shutdown
        ]
        self.num_active_panels = len(active_panels)
        if self.num_active_panels < 1:
            raise NoActivePanelsError(logger=self.logger)

        # Successfully initialized
        message = "Successfully initialized with {} ".format(
            self.num_active_panels)
        message2 = "active panels, expected {}".format(
            self.num_expected_panels)
        self.logger.debug(message + message2)

    def turn_on(self) -> Dict[str, float]:
        """Turns on leds."""
        self.logger.debug("Turning on")
        channel_outputs = self.build_channel_outputs(100)
        self.set_outputs(channel_outputs)

        return channel_outputs

    def turn_off(self) -> Dict[str, float]:
        """Turns off leds."""
        self.logger.debug("Turning off")
        channel_outputs = self.build_channel_outputs(0)
        self.set_outputs(channel_outputs)
        return channel_outputs

    def set_spd(
        self, desired_distance: float, desired_intensity: float,
        desired_spectrum: Dict
    ) -> Tuple[Optional[Dict], Optional[Dict], Optional[Dict]]:
        """Sets spectral power distribution."""
        message = "Setting spd, distance={}cm, ppfd={}umol/m2/s, spectrum={}".format(
            desired_distance, desired_intensity, desired_spectrum)
        self.logger.debug(message)

        # Approximate spectral power distribution
        try:
            channel_outputs, output_spectrum, output_intensity = light.approximate_spd(
                self.panel_properties,
                desired_distance,
                desired_intensity,
                desired_spectrum,
            )
        except Exception as e:
            message = "approximate spd failed"
            raise SetSPDError(message=message, logger=self.logger) from e

        # Set outputs
        self.set_outputs(channel_outputs)

        # Successfully set channel outputs
        message = "Successfully set spd, output: channels={}, spectrum={}, intensity={}umol/m2/s".format(
            channel_outputs, output_spectrum, output_intensity)
        self.logger.debug(message)
        return (channel_outputs, output_spectrum, output_intensity)

    def set_outputs(self, par_setpoints: dict) -> None:
        """Sets outputs on light panels. Converts channel names to channel numbers, 
        translates par setpoints to dac setpoints, then sets dac."""
        self.logger.debug("Setting outputs: {}".format(par_setpoints))

        # Check at least one panel is active
        active_panels = [
            panel for panel in self.panels if not panel.is_shutdown
        ]
        self.num_active_panels = len(active_panels)
        if self.num_active_panels < 1:
            raise NoActivePanelsError(logger=self.logger)
        message = "Setting outputs on {} active panels".format(
            self.num_active_panels)
        self.logger.debug(message)

        # Convert channel names to channel numbers
        converted_outputs = {}
        for name, percent in par_setpoints.items():

            # Convert channel name to channel number
            try:
                number = self.get_channel_number(name)
            except Exception as e:
                raise SetOutputsError(logger=self.logger) from e

            # Append to converted outputs
            converted_outputs[number] = percent

        # Try to set outputs on all panels
        for panel in self.panels:

            # Scale setpoints
            dac_setpoints = self.translate_setpoints(converted_outputs)

            # Set outputs on panel
            try:
                panel.driver.write_outputs(dac_setpoints)  # type: ignore
            except AttributeError:
                message = "Unable to set outputs on `{}`".format(panel.name)
                self.logger.error(message + ", panel not initialized")
            except Exception as e:
                message = "Unable to set outputs on `{}`".format(panel.name)
                self.logger.exception(message)
                panel.is_shutdown = True

                # TODO: Check for new events in manager
                # Manager event functions can get called any time
                # As a special case, use function to set a new_event flag in driver
                # Check it here and break if panel failed
                # Only on panel failures b/c only ultra case leading to high latency..
                # at 5 seconds per failed panel (due to retry)
                # In a grid like SMHC, 25 panels * 5 seconds is grueling...
                # How to clear flag?...event handler process funcs all remove it

        # Check at least one panel is still active
        active_panels = [
            panel for panel in self.panels if not panel.is_shutdown
        ]
        self.num_active_panels = len(active_panels)
        if self.num_active_panels < 1:
            message = "failed when setting outputs"
            raise NoActivePanelsError(message=message, logger=self.logger)

    def set_output(self, channel_name: str, par_setpoint: float) -> None:
        """Sets output on light panels. Converts channel name to channel number, 
        translates par setpoint to dac setpoint, then sets dac."""
        self.logger.debug("Setting ch {}: {}".format(channel_name,
                                                     par_setpoint))

        # Check at least one panel is active
        active_panels = [
            panel for panel in self.panels if not panel.is_shutdown
        ]
        if len(active_panels) < 1:
            raise NoActivePanelsError(logger=self.logger)
        message = "Setting output on {} active panels".format(
            self.num_active_panels)
        self.logger.debug(message)

        # Convert channel name to channel number
        try:
            channel_number = self.get_channel_number(channel_name)
        except Exception as e:
            raise SetOutputError(logger=self.logger) from e

        # Set output on all panels
        for panel in self.panels:

            # Scale setpoint
            dac_setpoint = self.translate_setpoint(par_setpoint)

            # Set output on panel
            try:
                panel.driver.write_output(channel_number,
                                          dac_setpoint)  # type: ignore
            except AttributeError:
                message = "Unable to set output on `{}`".format(panel.name)
                self.logger.error(message + ", panel not initialized")
            except Exception as e:
                message = "Unable to set output on `{}`".format(panel.name)
                self.logger.exception(message)
                panel.is_shutdown = True

        # Check at least one panel is still active
        active_panels = [
            panel for panel in self.panels if not panel.is_shutdown
        ]
        self.num_active_panels = len(active_panels)
        if self.num_active_panels < 1:
            message = "failed when setting output"
            raise NoActivePanelsError(message=message, logger=self.logger)

    def get_channel_number(self, channel_name: str) -> int:
        """Gets channel number from channel name."""
        try:
            channel_dict = self.channels[channel_name]  # type: ignore
            channel_number = channel_dict.get("port", -1)
            return int(channel_number)
        except KeyError:
            raise InvalidChannelNameError(message=channel_name,
                                          logger=self.logger)

    def get_channels(self) -> Dict:
        return self.channels

    def build_channel_outputs(self, value: float) -> Dict[str, float]:
        """Build channel outputs. Sets each channel to provided value."""
        self.logger.debug("Building channel outputs")
        channel_outputs = {}
        for key in self.channels.keys():  # type: ignore
            channel_outputs[key] = value
        self.logger.debug("channel outputs = {}".format(channel_outputs))
        return channel_outputs

    def translate_setpoints(self, par_setpoints: Dict) -> Dict:
        """Translates par setpoints to dac setpoints."""
        self.logger.debug("Translating setpoints")

        # Build interpolation lists
        dac_list = []
        par_list = []
        for dac_percent, par_percent in self.dac_map.items():  # type: ignore
            dac_list.append(float(dac_percent))
            par_list.append(float(par_percent))

        self.logger.debug("dac_list = {}".format(dac_list))
        self.logger.debug("par_list = {}".format(par_list))

        # Get dac setpoints
        dac_setpoints = {}
        for key, par_setpoint in par_setpoints.items():
            dac_setpoint = maths.interpolate(par_list, dac_list, par_setpoint)
            dac_setpoints[key] = dac_setpoint

        # Successfully translated dac setpoints
        self.logger.debug("Translated setpoints from {} to {}".format(
            par_setpoints, dac_setpoints))
        return dac_setpoints

    def translate_setpoint(self, par_setpoint: float) -> float:
        """Translates par setpoint to dac setpoint."""

        # Build interpolation lists
        dac_list = []
        par_list = []
        for dac_percent, par_percent in self.dac_map.items():  # type: ignore
            dac_list.append(float(dac_percent))
            par_list.append(float(par_percent))

        # Get dac setpint
        dac_setpoint = maths.interpolate(par_list, dac_list, par_setpoint)

        # Successfully translated dac setpoint
        return dac_setpoint  # type: ignore