Example #1
0
    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
Example #2
0
    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
Example #3
0
    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)
Example #4
0
    def __init__(
        self,
        name: str,
        bus: int,
        device_addr: int,
        mux_address: Optional[int],
        mux_channel: Optional[int],
        mux_simulator: Optional[MuxSimulator],
    ) -> None:

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

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

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

        # Initialize register
        self.registers: Dict[int, int] = {}
        self.writes: Dict[str, bytes] = {}
    def __init__(
        self,
        name: str,
        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")
Example #6
0
    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.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
    def __init__(self, name: str, bus: int) -> None:

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

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

        # Verify io exists
        self.logger.debug("Verifying io stream exists")
        self.open()
        self.close()
    def __init__(
        self,
        name: str,
        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)
Example #9
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)
    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()
Example #11
0
    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
Example #12
0
    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")
Example #13
0
    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 __init__(self,
                 name: str,
                 simulate: bool = False,
                 ini_file: str = None,
                 config_file: str = None,
                 debug: bool = False) -> None:
        """Initializes bacpypes."""

        self.logger = Logger(name + ".BACNet", __name__)

        if ini_file is None or config_file is None:
            raise exceptions.InitError(message="Missing file args",
                                       logger=self.logger)

        try:
            self.logger.info("driver init")
            self.bnet = BACNET.Bnet(self.logger, ini_file, config_file, debug)

        except Exception as e:
            raise exceptions.InitError(logger=self.logger) from e
Example #15
0
    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
Example #16
0
    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 __init__(self) -> None:
     """Initializes state machine manager."""
     self.logger: Logger = Logger("StateMachineManager", __name__)
     self.thread: threading.Thread = threading.Thread(target=self.run)
     self.event_queue: queue.Queue = queue.Queue()
     self.is_shutdown: bool = False
     self._mode: str = modes.INIT
     self.transitions: Dict[str, List[str]] = {
         modes.INIT: [modes.NORMAL, modes.SHUTDOWN, modes.ERROR],
         modes.NORMAL: [modes.RESET, modes.SHUTDOWN, modes.ERROR],
         modes.RESET: [modes.INIT, modes.SHUTDOWN, modes.ERROR],
         modes.ERROR: [modes.RESET, modes.SHUTDOWN],
     }
     self.logger.debug("Initialized")
Example #18
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 #19
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 #20
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 #21
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
Example #22
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 #23
0
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 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 #25
0
    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
Example #26
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
# Import device utilities
from device.utilities.logger import Logger

# Import driver elements
from device.peripherals.modules.bacnet import exceptions

# Conditionally import the bacpypes wrapper class, or use the simulator.
# The brain that runs on PFCs doesn't have or need BACnet communications,
# only the LGHC (running on linux) does.
try:
    from device.peripherals.modules.bacnet import bnet_wrapper as BACNET
except Exception as e:
    l = Logger("\n\nBACNet.driver", __name__)
    l.critical(e)
    from device.peripherals.modules.bacnet import bnet_simulator as BACNET


class BacnetDriver:
    """Driver for BACNet communications to HVAC."""

    # --------------------------------------------------------------------------
    def __init__(self,
                 name: str,
                 simulate: bool = False,
                 ini_file: str = None,
                 config_file: str = None,
                 debug: bool = False) -> None:
        """Initializes bacpypes."""

        self.logger = Logger(name + ".BACNet", __name__)
Example #28
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
Example #29
0
# Import device utilities
from device.utilities.logger import Logger
from device.utilities import network

# Initialize file paths
REGISTRATION_DATA_DIR = "data/registration/"
DEVICE_ID_PATH = REGISTRATION_DATA_DIR + "device_id.bash"
ROOTS_PATH = REGISTRATION_DATA_DIR + "roots.pem"
RSA_CERT_PATH = REGISTRATION_DATA_DIR + "rsa_cert.pem"
RSA_PRIVATE_PATH = REGISTRATION_DATA_DIR + "rsa_private.pem"
VERIFICATION_CODE_PATH = REGISTRATION_DATA_DIR + "verification_code.txt"
REGISTER_SCRIPT_PATH = "scripts/one_time_key_creation_and_iot_device_registration.sh"

# Initialize logger
logger = Logger("IotRegistrationUtility", "iot")


def is_registered() -> bool:
    """Checks if device is registered by checking local files."""
    logger.debug("Checking if device is registered")
    if (os.path.exists(DEVICE_ID_PATH) and os.path.exists(ROOTS_PATH)
            and os.path.exists(RSA_CERT_PATH)
            and os.path.exists(RSA_PRIVATE_PATH)):
        return True
    else:
        return False


def device_id() -> str:
    """Gets device id string from local file. TODO: Handle exeptions."""
class BacnetDriver:
    """Driver for BACNet communications to HVAC."""

    # --------------------------------------------------------------------------
    def __init__(self,
                 name: str,
                 simulate: bool = False,
                 ini_file: str = None,
                 config_file: str = None,
                 debug: bool = False) -> None:
        """Initializes bacpypes."""

        self.logger = Logger(name + ".BACNet", __name__)

        if ini_file is None or config_file is None:
            raise exceptions.InitError(message="Missing file args",
                                       logger=self.logger)

        try:
            self.logger.info("driver init")
            self.bnet = BACNET.Bnet(self.logger, ini_file, config_file, debug)

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

    # --------------------------------------------------------------------------
    def setup(self) -> None:
        self.bnet.setup()

    # --------------------------------------------------------------------------
    def reset(self) -> None:
        self.bnet.reset()

    # --------------------------------------------------------------------------
    # This peripheral thread has been killed by the periph. manager. dead.
    def shutdown(self) -> None:
        self.logger.info("shutdown")

    # --------------------------------------------------------------------------
    def ping(self) -> None:
        self.bnet.ping()

    # --------------------------------------------------------------------------
    def set_test_voltage(self, voltage: float) -> None:
        if voltage is None or voltage < 0.0 or voltage > 100.0:
            self.logger.error(f"Test voltage {voltage} out of range (0-100%)")
            return
        self.bnet.set_test_voltage(voltage)

    # --------------------------------------------------------------------------
    def set_air_temp(self, tempC: float) -> None:
        if tempC is None or tempC < -100.0 or tempC > 200.0:
            self.logger.error(f"Air Temperature Celsius {tempC} out of range")
            return
        self.bnet.set_air_temp(tempC)

    # --------------------------------------------------------------------------
    def set_air_RH(self, RH: float) -> None:
        if RH is None or RH < 0.0 or RH > 100.0:
            self.logger.error(f"Relative Humidity {RH} out of range")
            return
        self.bnet.set_air_RH(RH)

    # --------------------------------------------------------------------------
    def get_air_temp(self) -> float:
        return self.bnet.get_air_temp()

    # --------------------------------------------------------------------------
    def get_air_RH(self) -> float:
        return self.bnet.get_air_RH()