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)
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)
class DAC5578Driver: """Driver for DAC5578 digital to analog converter.""" def __init__( self, name: str, i2c_lock: threading.RLock, bus: int, address: int, mux: Optional[int] = None, channel: Optional[int] = None, simulate: bool = False, mux_simulator: Optional[MuxSimulator] = None, ) -> None: """Initializes DAC5578.""" # Initialize logger logname = "DAC5578-({})".format(name) self.logger = Logger(logname, __name__) # Check if simulating if simulate: self.logger.info("Simulating driver") Simulator = DAC5578Simulator else: Simulator = None # Initialize I2C try: self.i2c = I2C( name="DAC5578-{}".format(name), i2c_lock=i2c_lock, bus=bus, address=address, mux=mux, channel=channel, mux_simulator=mux_simulator, PeripheralSimulator=Simulator, ) except I2CError as e: raise exceptions.InitError(logger=self.logger) from e def write_output(self, channel: int, percent: int, retry: bool = True, disable_mux: bool = False) -> None: """Sets output value to channel.""" message = "Writing output on channel {} to: {:.02F}%".format( channel, percent) self.logger.debug(message) # Check valid channel range if channel < 0 or channel > 7: message = "channel out of range, must be within 0-7" raise exceptions.WriteOutputError(message=message, logger=self.logger) # Check valid value range if percent < 0 or percent > 100: message = "output percent out of range, must be within 0-100" raise exceptions.WriteOutputError(message=message, logger=self.logger) # Convert output percent to byte, ensure 100% is byte 255 if percent == 100: byte = 255 else: byte = int(percent * 2.55) # Send set output command to dac self.logger.debug("Writing to dac: ch={}, byte={}".format( channel, byte)) try: self.i2c.write(bytes([0x30 + channel, byte, 0x00]), disable_mux=disable_mux) except I2CError as e: raise exceptions.WriteOutputError(logger=self.logger) from e def write_outputs(self, outputs: dict, retry: bool = True) -> None: """Sets output channels to output percents. Only sets mux once. Keeps thread locked since relies on mux not changing.""" self.logger.debug("Writing outputs: {}".format(outputs)) # Check output dict is not empty if len(outputs) < 1: message = "output dict must not be empty" raise exceptions.WriteOutputsError(message=message, logger=self.logger) if len(outputs) > 8: print("outputs len = {}".format(len(outputs))) message = "output dict must not contain more than 8 entries" raise exceptions.WriteOutputsError(message=message, logger=self.logger) # Run through each output for channel, percent in outputs.items(): message = "Writing output for ch {}: {}%".format(channel, percent) self.logger.debug(message) try: self.write_output(channel, percent, retry=retry) except exceptions.WriteOutputError as e: raise exceptions.WriteOutputsError(logger=self.logger) from e def read_power_register(self, retry: bool = True) -> Optional[Dict[int, bool]]: """Reads power register.""" self.logger.debug("Reading power register") # Read register try: self.i2c.write([0x40], retry=retry) bytes_ = self.i2c.read(2, retry=retry) except I2CError as e: raise exceptions.ReadPowerRegisterError(logger=self.logger) from e # Parse response bytes msb = bytes_[0] lsb = bytes_[1] powered_channels = { 0: not bool(bitwise.get_bit_from_byte(4, msb)), 1: not bool(bitwise.get_bit_from_byte(3, msb)), 2: not bool(bitwise.get_bit_from_byte(2, msb)), 3: not bool(bitwise.get_bit_from_byte(1, msb)), 4: not bool(bitwise.get_bit_from_byte(0, msb)), 5: not bool(bitwise.get_bit_from_byte(7, lsb)), 6: not bool(bitwise.get_bit_from_byte(6, lsb)), 7: not bool(bitwise.get_bit_from_byte(5, lsb)), } return powered_channels def set_high(self, channel: Optional[int] = None, retry: bool = True) -> None: """Sets channel high, sets all channels high if no channel is specified.""" if channel != None: self.logger.debug("Setting channel {} high".format(channel)) try: self.write_output(channel, 100, retry=retry) # type: ignore except exceptions.WriteOutputError as e: raise exceptions.SetHighError(logger=self.logger) from e else: self.logger.debug("Setting all channels high") outputs = { 0: 100, 1: 100, 2: 100, 3: 100, 4: 100, 5: 100, 6: 100, 7: 100 } try: self.write_outputs(outputs, retry=retry) except exceptions.WriteOutputsError as e: raise exceptions.SetHighError(logger=self.logger) from e def set_low(self, channel: Optional[int] = None, retry: bool = True) -> None: """Sets channel low, sets all channels low if no channel is specified.""" if channel != None: self.logger.debug("Setting channel {} low".format(channel)) try: self.write_output(channel, 100, retry=retry) # type: ignore except exceptions.WriteOutputError as e: raise exceptions.SetLowError(logger=self.logger) from e else: self.logger.debug("Setting all channels low") outputs = {0: 0, 1: 0, 2: 0, 3: 0, 4: 0, 5: 0, 6: 0, 7: 0} try: self.write_outputs(outputs, retry=retry) except exceptions.WriteOutputsError as e: raise exceptions.SetLowError(logger=self.logger) from e
class USBCameraDriver: """Driver for a usb camera.""" def __init__( self, name: str, vendor_id: int, product_id: int, resolution: str, simulate: bool = False, usb_mux_comms: Optional[Dict[str, Any]] = None, usb_mux_channel: Optional[int] = None, i2c_lock: Optional[threading.Lock] = None, mux_simulator: Optional[MuxSimulator] = None, ) -> None: """Initializes USB camera camera.""" # Initialize parameters self.name = name self.vendor_id = vendor_id self.product_id = product_id self.resolution = resolution self.simulate = simulate # Initialize logger self.logger = Logger(name="Driver({})".format(name), dunder_name=__name__) # Check if simulating if simulate: self.logger.info("Simulating driver") self.directory = "device/peripherals/modules/usb_camera/tests/images/" else: self.directory = IMAGE_DIR # Check directory exists else create it if not os.path.exists(self.directory): os.makedirs(self.directory) # Check if using usb mux if usb_mux_comms == None or usb_mux_channel == None: self.dac5578 = None return # Get optional i2c parameters mux = usb_mux_comms.get("mux", None) # type: ignore if mux != None: mux = int(mux, 16) # Using usb mux, initialize driver try: self.dac5578 = DAC5578Driver( name=name, i2c_lock=i2c_lock, # type: ignore bus=usb_mux_comms.get("bus", None), # type: ignore address=int(usb_mux_comms.get("address", None), 16), # type: ignore mux=mux, channel=usb_mux_comms.get("channel", None), # type: ignore simulate=simulate, mux_simulator=mux_simulator, ) self.usb_mux_channel = usb_mux_channel except I2CError as e: raise InitError(logger=self.logger) from e def list_cameras(self, vendor_id: Optional[int] = None, product_id: Optional[int] = None) -> List[str]: """Returns list of cameras that match the provided vendor id and product id.""" # List all cameras cameras = glob.glob("/dev/video*") # Check if filtering by product and vendor id if vendor_id == None and product_id == None: return cameras # Check for product and vendor id matches matches = [] for camera in cameras: if usb_device_matches(camera, vendor_id, product_id): matches.append(camera) return matches def get_camera(self) -> str: """Gets camera path.""" # Get camera paths that match vendor and product ID cameras = self.list_cameras(self.vendor_id, self.product_id) # Check only one active camera if len(cameras) < 1: message = "no active cameras" raise GetCameraError(message=message, logger=self.logger) elif len(cameras) > 1: message = "too many active cameras" raise GetCameraError(message=message, logger=self.logger) # Successfuly got one camera return cameras[0] def enable_camera(self, retry: bool = True) -> None: """Enables camera by setting dac output high.""" self.logger.debug("Enabling camera") # Turn on usb mux channel try: self.dac5578.set_high(channel=self.usb_mux_channel, retry=retry) # type: ignore except DriverError as e: raise EnableCameraError(logger=self.logger) from e # Wait for camera to initialize time.sleep(5) def disable_camera(self, retry: bool = True) -> None: """Disables camera by setting dac output low.""" self.logger.debug("Disabling camera") # Turn off usb mux channel try: self.dac5578.set_low(channel=self.usb_mux_channel, retry=retry) # type: ignore except DriverError as e: raise DisableCameraError(logger=self.logger) from e # Wait for camera to power down start_time = time.time() while True: # 5 second timeout # Look for camera try: camera = self.get_camera() # Check if camera powered down if camera == None: self.logger.debug("Camera powered down") return # TODO: Handle specific exceptions except Exception as e: raise DisableCameraError(logger=self.logger) from e # Check for timeout if time.time() - start_time > 5: # 5 second timeout message = "timed out" raise DisableCameraError(message=message, logger=self.logger) # Update every 100ms time.sleep(0.1) def capture(self, retry: bool = True) -> None: """Manages usb mux and captures an image.""" # Check if not using usb mux if self.dac5578 == None: return self.capture_image() # TODO: Lock camera threads # Manage 'mux' and capture image try: self.enable_camera() self.capture_image() self.disable_camera() except DriverError as e: raise CaptureError(logger=self.logger) from e # TODO: Unlock camera threads def capture_image(self) -> None: """Captures an image.""" # Name image according to ISO8601 timestr = datetime.datetime.utcnow().strftime("%Y-%m-%d-T%H:%M:%SZ") filename = timestr + "_" + self.name + ".png" # Specify filepaths image_path = self.directory + filename base_path = "device/peripherals/modules/usb_camera" dummy_path = "{}/dummy.png".format(base_path) active_path = "{}/active.jpg".format(base_path) # Check if simulated if self.simulate: self.logger.info( "Simulating capture, saving simulation image to: {}".format( image_path)) command = "cp device/peripherals/modules/usb_camera/tests/simulation_image.png {}".format( image_path) os.system(command) return # Camera not simulated, get camera path try: camera = self.get_camera() except GetCameraError as e: raise CaptureImageError(logger=self.logger) from e # Capture image try: # Take 3 low res images to clear out buffer self.logger.debug("Capturing dummy images") command = "fswebcam -d {} -r '320x240' ".format(camera, dummy_path) for i in range(3): os.system(command) # Try taking up to 3 real images self.logger.debug("Capturing active image") command = "fswebcam -d {} -r {} --png 9 -F 10 --no-banner --save {}".format( camera, self.resolution, active_path) valid_image = False for i in range(3): os.system(command) size = os.path.getsize(active_path) # Check if image meets minimum size constraint # TODO: Check lighting conditions (if box is dark, images are small) if size > 160000: # 160kB valid_image = True break # Check if active image is valid, if so copy to images/ directory if not valid_image: self.logger.warning("Unable to capture a valid image") else: self.logger.info( "Captured image, saved to {}".format(image_path)) os.rename(active_path, image_path) except Exception as e: raise CaptureImageError(logger=self.logger) from e
class DeviceIO(object): """Manages byte-level device IO.""" def __init__(self, name: str, bus: int) -> None: # Initialize parameters self.name = name self.bus = bus # Initialize logger logname = "DeviceIO({})".format(name) self.logger = Logger(logname, __name__) # Verify io exists self.logger.debug("Verifying io stream exists") self.open() self.close() def __del__(self) -> None: """Clean up any resources used by the I2c instance.""" self.close() def __enter__(self) -> object: """Context manager enter function.""" return self def __exit__(self, exc_type: ET, exc_val: EV, exc_tb: EB) -> bool: """Context manager exit function, ensures resources are cleaned up.""" self.close() return False # Don't suppress exceptions def open(self) -> None: """Opens io stream.""" try: if os.getenv("IS_I2C_ENABLED") == "true": device_name = "/dev/i2c-{}".format(self.bus) self.io = io.open(device_name, "r+b", buffering=0) elif os.getenv("IS_USB_I2C_ENABLED") == "true": self.io = I2cController() self.io.configure("ftdi://ftdi:232h/1") # type: ignore else: message = "Platform does not support i2c communication" raise InitError(message) except (PermissionError, I2cIOError, I2cNackError) as e: message = "Unable to open device io: {}".format(device_name) raise InitError(message, logger=self.logger) from e def close(self) -> None: """Closes io stream.""" try: if os.getenv("IS_I2C_ENABLED") == "true": self.io.close() elif os.getenv("IS_USB_I2C_ENABLED") == "true": self.io.terminate() # type: ignore else: message = "Platform does not support i2c communication" raise InitError(message) except: self.logger.exception("Unable to close") @manage_io def write(self, address: int, bytes_: bytes) -> None: """ Writes bytes to IO stream. """ try: if os.getenv("IS_I2C_ENABLED") == "true": fcntl.ioctl(self.io, I2C_SLAVE, address) self.io.write(bytes_) elif os.getenv("IS_USB_I2C_ENABLED") == "true": device = self.io.get_port(address) # type: ignore device.write(bytes_) else: message = "Platform does not support i2c communication" raise WriteError(message) except (IOError, I2cIOError, I2cNackError) as e: message = "Unable to write: {}".format(bytes_) raise WriteError(message) from e @manage_io def read(self, address: int, num_bytes: int) -> bytes: """Reads bytes from io stream.""" try: if os.getenv("IS_I2C_ENABLED") == "true": fcntl.ioctl(self.io, I2C_SLAVE, address) return bytes(self.io.read(num_bytes)) elif os.getenv("IS_USB_I2C_ENABLED") == "true": device = self.io.get_port(address) # type: ignore bytes_ = device.read(readlen=num_bytes) return bytes(bytes_) else: message = "Platform does not support i2c communication" raise ReadError(message) except (IOError, I2cIOError, I2cNackError) as e: message = "Unable to read {} bytes".format(num_bytes) raise ReadError(message) from e @manage_io def read_register(self, address: int, register: int) -> int: """Reads register from io stream.""" try: if os.getenv("IS_I2C_ENABLED") == "true": reg = c_uint8(register) result = c_uint8() request = make_i2c_rdwr_data( # type: ignore [ (address, 0, 1, pointer(reg)), # write cmd register ( address, I2C_M_RD, 1, pointer(result), ), # read 1 byte as result ] ) fcntl.ioctl(self.io.fileno(), I2C_RDWR, request) byte_ = int(result.value) message = "Read register 0x{:02X}, value: 0x{:02X}".format( register, byte_ ) self.logger.debug(message) return byte_ elif os.getenv("IS_USB_I2C_ENABLED") == "true": device = self.io.get_port(address) # type: ignore byte_raw = device.read_from(register, readlen=1) byte = int(byte_raw[0]) return byte else: message = "Platform does not support i2c communication" raise ReadError(message) except (IOError, I2cIOError, I2cNackError) as e: message = "Unable to read register 0x{:02}".format(register) raise ReadError(message) from e @manage_io def write_register(self, address: int, register: int, value: int) -> None: """ Writes bytes to IO stream. """ # Check register within range if register not in range(256): message = "Invalid register addrress: {}, must be 0-255".format(register) raise WriteError(message) # Check value within range if value not in range(256): message = "Invalid register value: {}, must be 0-255".format(value) raise WriteError(message) # Write to register try: if os.getenv("IS_I2C_ENABLED") == "true": self.write(address, bytes([register, value])) elif os.getenv("IS_USB_I2C_ENABLED") == "true": device = self.io.get_port(address) # type: ignore device.write_to(register, [value]) else: message = "Platform does not support i2c communication" raise WriteError(message) except (IOError, I2cIOError, I2cNackError) as e: message = "Unable to write register 0x{:02}".format(register) raise WriteError(message) from e
class DeviceIO(object): """Manages byte-level device IO. Attributes: name -- name of device bus -- device i2c bus """ def __init__(self, name: str, bus: int) -> None: # Initialize parameters self.name = name self.bus = bus # Initialize logger logname = "DeviceIO({})".format(name) self.logger = Logger(logname, __name__) # Verify io exists self.logger.debug("Verifying io stream exists") self.open() self.close() def __del__(self) -> None: """Clean up any resources used by the I2c instance.""" self.close() def __enter__(self) -> object: """Context manager enter function.""" return self def __exit__(self, exc_type: ET, exc_val: EV, exc_tb: EB) -> bool: """Context manager exit function, ensures resources are cleaned up.""" self.close() return False # Don't suppress exceptions def open(self) -> None: """Opens io stream.""" try: device_name = "/dev/i2c-{}".format(self.bus) self.io = io.open(device_name, "r+b", buffering=0) except PermissionError as e: message = "Unable to open device io: {}".format(device_name) raise InitError(message, logger=self.logger) from e def close(self) -> None: """Closes io stream.""" try: self.io.close() except: self.logger.exception("Unable to close") @manage_io def write(self, address: int, bytes_: bytes) -> None: """ Writes bytes to IO stream. """ try: fcntl.ioctl(self.io, I2C_SLAVE, address) self.io.write(bytes_) except IOError as e: message = "Unable to write: {}".format(bytes_) raise WriteError(message) from e @manage_io def read(self, address: int, num_bytes: int) -> bytes: """Reads bytes from io stream.""" try: fcntl.ioctl(self.io, I2C_SLAVE, address) return bytes(self.io.read(num_bytes)) except IOError as e: message = "Unable to read {} bytes".format(num_bytes) raise ReadError(message) from e @manage_io def read_register(self, address: int, register: int) -> int: """Reads register from io stream.""" try: reg = c_uint8(register) result = c_uint8() request = make_i2c_rdwr_data( # type: ignore [ (address, 0, 1, pointer(reg)), # write cmd register (address, I2C_M_RD, 1, pointer(result)), # read 1 byte as result ]) fcntl.ioctl(self.io.fileno(), I2C_RDWR, request) byte_ = int(result.value) message = "Read register 0x{:02X}, value: 0x{:02X}".format( register, byte_) self.logger.debug(message) return byte_ except IOError as e: message = "Unable to read register 0x{:02}".format(register) raise ReadError(message) from e @manage_io def write_register(self, address: int, register: int, value: int) -> None: """ Writes bytes to IO stream. """ # Check register within range if register not in range(256): message = "Invalid register addrress: {}, must be 0-255".format( register) raise WriteError(message) # Check value within range if value not in range(256): message = "Invalid register value: {}, must be 0-255".format(value) raise WriteError(message) # Write to register self.write(address, bytes([register, value]))
class ArduinoCommsDriver: def __init__( self, name: str, i2c_lock: threading.RLock, ) -> None: """Initializes ArduinoComms""" # Initialize logger logname = "ArduinoComms({})".format(name) self.logger = Logger(logname, __name__) self.bus = 2 self.address = 0x08 self.name = name # Initialize I2C try: self.i2c = I2C(name="Arduino-{}".format(name), i2c_lock=i2c_lock, bus=self.bus, address=self.address, mux=None) except I2CError as e: raise exceptions.InitError(logger=self.logger) from e def write_output(self, value, pin=None, pins=None): if pin: self.i2c.write(bytes("*w_{}_{}^".format(pin, value), 'utf-8')) self.logger.debug("{} write_output: *w_{}_{}^".format( self.name, pin, value)) elif pins: for p in pins: self.i2c.write(bytes("*w_{}_{}^".format(p, value), 'utf-8')) self.logger.debug("{} write_output: *w_{}_{}^".format( self.name, p, value)) time.sleep(.1) else: raise exceptions.WriteOutputError(logger=self.logger) # for pin in self.pins: # self.logger.debug("{} write_output: *w_{}_{}^".format(self.name, pin, value)) # self.i2c.write(bytes("*w_{}_{}^".format(pin, value), 'utf-8')) # def read_register(self, register): # self.i2c.write(bytes("*r_{}_{}^".format(self.address, register), 'utf-8')) # # num_bytes = 1 # self.i2c.read(num_bytes) # # def write_register(self, register, message): # self.i2c.write(bytes("*r_{}_{}_{}^".format(self.address, register, message), 'utf-8')) def write_outputs(self, output_values): for pin, value in output_values.items(): self.write_output(value, pin=pin) time.sleep(.1) def set_high(self, pin=None, pins=None) -> None: if pin: self.write_output(255, pin=pin) self.logger.debug("Setting pin {} high".format(pin)) elif pins: self.write_output(255, pins=pin) self.logger.debug("Setting pins {} high".format(pins)) time.sleep(.1) else: raise exceptions.SetHighError(logger=self.logger) # try: # self.write_output(1) # type: ignore # except exceptions.WriteOutputError as e: # raise exceptions.SetHighError(logger=self.logger) from e def set_low(self, pin=None, pins=None) -> None: if pin: self.write_output(0, pin=pin) self.logger.debug("Setting pins {} low".format(pin)) elif pins: self.write_output(0, pin=pins) self.logger.debug("Setting pins {} low".format(pins)) time.sleep(.1) else: raise exceptions.SetLowError(logger=self.logger)
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
# 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))
class LED: # -------------------------------------------------------------------------- # Constants for PCA9632 PCA9632_I2C_ADDRESS = 0x62 R_BYTE = 3 G_BYTE = 4 B_BYTE = 5 # -------------------------------------------------------------------------- def __init__( self, bus: int = 0, address: int = PCA9632_I2C_ADDRESS, mux: Optional[int] = None, channel: Optional[int] = None, simulate: bool = False, mux_simulator: Optional[MuxSimulator] = None, ) -> None: """Initializes Grove RGB LCD.""" # Initialize logger self.logger = Logger('LED', __name__) # Check if simulating if simulate: self.logger.info("Simulating LED") # Initialize I2C try: self.i2c = I2C( name="LED", i2c_lock=threading.RLock(), bus=bus, address=address, mux=mux, channel=channel, mux_simulator=mux_simulator, PeripheralSimulator=None, ) except I2CError as e: raise LEDError(logger=self.logger) from e # Initialize the LED try: self.init_data = [ 0x80, 0x80, 0x21, 0x00, 0x00, 0x00, 0x40, 0x80, 0x02, 0xEA ] # init and clear any LED values self.i2c.write(bytes(self.init_data)) except I2CError as e: raise LEDError(logger=self.logger) from e # -------------------------------------------------------------------------- # Turn off all LEDs def off(self): try: data = self.init_data data[self.R_BYTE] = 0x00 data[self.G_BYTE] = 0x00 data[self.B_BYTE] = 0x00 self.i2c.write(bytes(data)) except I2CError as e: raise LEDError(logger=self.logger) from e # -------------------------------------------------------------------------- def set(self, R: int = 0x00, G: int = 0x00, B: int = 0x00) -> None: # validate the inputs are 0 <> 255 if R < 0 or R > 255 or G < 0 or G > 255 or B < 0 or B > 255: self.logger.error("RGB values must be between 0 and 255") raise LEDError(logger=self.logger) message = "Setting LED: {:2X}, {:2X}, {:2X}".format(R, G, B) self.logger.debug(message) # Set the backlight RGB value try: data = self.init_data data[self.R_BYTE] = R data[self.G_BYTE] = G data[self.B_BYTE] = B self.i2c.write(bytes(data)) except I2CError as e: raise LEDError(logger=self.logger) from e
class RecipeManager(StateMachineManager): """Manages recipe state machine thread.""" def __init__(self, state: State) -> None: """Initializes recipe manager.""" # Initialize parent class super().__init__() # Initialize logger self.logger = Logger("Recipe", "recipe") # Initialize state self.state = state # Initialize state machine transitions self.transitions = { modes.INIT: [modes.NORECIPE, modes.ERROR], modes.NORECIPE: [modes.START, modes.ERROR], modes.START: [modes.QUEUED, modes.ERROR], modes.QUEUED: [modes.NORMAL, modes.STOP, modes.ERROR], modes.NORMAL: [modes.PAUSE, modes.STOP, modes.ERROR], modes.PAUSE: [modes.START, modes.ERROR], modes.STOP: [modes.NORECIPE, modes.ERROR], modes.ERROR: [modes.RESET], modes.RESET: [modes.INIT], } # Start state machine from init mode self.mode = modes.INIT @property def mode(self) -> str: """Gets mode value. Important to keep this local so all state transitions only occur from within thread.""" return self._mode @mode.setter def mode(self, value: str) -> None: """Safely updates recipe mode in shared state.""" self._mode = value with self.state.lock: self.state.recipe["mode"] = value @property def stored_mode(self) -> Optional[str]: """Gets the stored mode from shared state.""" value = self.state.recipe.get("stored_mode") if value != None: return str(value) else: return None @stored_mode.setter def stored_mode(self, value: Optional[str]) -> None: """Safely updates stored mode in shared state.""" with self.state.lock: self.state.recipe["stored_mode"] = value @property def recipe_uuid(self) -> Optional[str]: """Gets recipe uuid from shared state.""" value = self.state.recipe.get("recipe_uuid") if value != None: return str(value) else: return None @recipe_uuid.setter def recipe_uuid(self, value: Optional[str]) -> None: """Safely updates recipe uuid in shared state.""" with self.state.lock: self.state.recipe["recipe_uuid"] = value @property def recipe_name(self) -> Optional[str]: """Gets recipe name from shared state.""" value = self.state.recipe.get("recipe_name") if value != None: return str(value) else: return None @recipe_name.setter def recipe_name(self, value: Optional[str]) -> None: """ afely updates recipe name in shared state.""" with self.state.lock: self.state.recipe["recipe_name"] = value @property def is_active(self) -> bool: """Gets value.""" return self.state.recipe.get("is_active", False) # type: ignore @is_active.setter def is_active(self, value: bool) -> None: """Safely updates value in shared state.""" with self.state.lock: self.state.recipe["is_active"] = value @property def current_timestamp_minutes(self) -> int: """ Get current timestamp in minutes. """ return int(time.time() / 60) @property def start_timestamp_minutes(self) -> Optional[int]: """ Gets start timestamp minutes from shared state. """ value = self.state.recipe.get("start_timestamp_minutes") if value != None: return int(value) # type: ignore else: return None @start_timestamp_minutes.setter def start_timestamp_minutes(self, value: Optional[int]) -> None: """Generates start datestring then safely updates start timestamp minutes and datestring in shared state.""" # Define var type start_datestring: Optional[str] # Generate start datestring if value != None: val_int = int(value) # type: ignore start_datestring = ( datetime.datetime.fromtimestamp(val_int * 60).strftime( "%Y-%m-%d %H:%M:%S" ) + " UTC" ) else: start_datestring = None # Update start timestamp minutes and datestring in shared state with self.state.lock: self.state.recipe["start_timestamp_minutes"] = value self.state.recipe["start_datestring"] = start_datestring @property def start_datestring(self) -> Optional[str]: """Gets start datestring value from shared state.""" return self.state.recipe.get("start_datestring") # type: ignore @property def duration_minutes(self) -> Optional[int]: """Gets recipe duration in minutes from shared state.""" return self.state.recipe.get("duration_minutes") # type: ignore @duration_minutes.setter def duration_minutes(self, value: Optional[int]) -> None: """Generates duration string then safely updates duration string and minutes in shared state. """ # Define var type duration_string: Optional[str] # Generate duation string if value != None: duration_string = self.get_duration_string(value) # type: ignore else: duration_string = None # Safely update duration minutes and string in shared state with self.state.lock: self.state.recipe["duration_minutes"] = value self.state.recipe["duration_string"] = duration_string @property def last_update_minute(self) -> Optional[int]: """Gets the last update minute from shared state.""" return self.state.recipe.get("last_update_minute") # type: ignore @last_update_minute.setter def last_update_minute(self, value: Optional[int]) -> None: """Generates percent complete, percent complete string, time remaining minutes, time remaining string, and time elapsed string then safely updates last update minute and aforementioned values in shared state. """ # Define var types percent_complete_string: Optional[float] time_remaining_string: Optional[str] time_elapsed_string: Optional[str] # Generate values if value != None and self.duration_minutes != None: percent_complete = ( float(value) / self.duration_minutes * 100 # type: ignore ) percent_complete_string = "{0:.2f} %".format( # type: ignore percent_complete ) time_remaining_minutes = self.duration_minutes - value # type: ignore time_remaining_string = self.get_duration_string(time_remaining_minutes) time_elapsed_string = self.get_duration_string(value) # type: ignore else: percent_complete = None # type: ignore percent_complete_string = None time_remaining_minutes = None time_remaining_string = None time_elapsed_string = None # Safely update values in shared state with self.state.lock: self.state.recipe["last_update_minute"] = value self.state.recipe["percent_complete"] = percent_complete self.state.recipe["percent_complete_string"] = percent_complete_string self.state.recipe["time_remaining_minutes"] = time_remaining_minutes self.state.recipe["time_remaining_string"] = time_remaining_string self.state.recipe["time_elapsed_string"] = time_elapsed_string @property def percent_complete(self) -> Optional[float]: """Gets percent complete from shared state.""" return self.state.recipe.get("percent_complete") # type: ignore @property def percent_complete_string(self) -> Optional[str]: """Gets percent complete string from shared state.""" return self.state.recipe.get("percent_complete_string") # type: ignore @property def time_remaining_minutes(self) -> Optional[int]: """Gets time remaining minutes from shared state.""" return self.state.recipe.get("time_remaining_minutes") # type: ignore @property def time_remaining_string(self) -> Optional[str]: """Gets time remaining string from shared state.""" return self.state.recipe.get("time_remaining_string") # type: ignore @property def time_elapsed_string(self) -> Optional[str]: """Gets time elapsed string from shared state.""" value = self.state.recipe.get("time_elapsed_string") if value != None: return str(value) else: return None @property def current_phase(self) -> Optional[str]: """Gets the recipe current phase from shared state.""" value = self.state.recipe.get("current_phase") if value != None: return str(value) else: return None @current_phase.setter def current_phase(self, value: str) -> None: """Safely updates current phase in shared state.""" with self.state.lock: self.state.recipe["current_phase"] = value @property def current_cycle(self) -> Optional[str]: """Gets the current cycle from shared state.""" value = self.state.recipe.get("current_cycle") if value != None: return str(value) else: return None @current_cycle.setter def current_cycle(self, value: Optional[str]) -> None: """Safely updates current cycle in shared state.""" with self.state.lock: self.state.recipe["current_cycle"] = value @property def current_environment_name(self) -> Optional[str]: """Gets the current environment name from shared state""" value = self.state.recipe.get("current_environment_name") if value != None: return str(value) else: return None @current_environment_name.setter def current_environment_name(self, value: Optional[str]) -> None: """Safely updates current environment name in shared state.""" with self.state.lock: self.state.recipe["current_environment_name"] = value @property def current_environment_state(self) -> Any: """Gets the current environment state from shared state.""" return self.state.recipe.get("current_environment_name") @current_environment_state.setter def current_environment_state(self, value: Optional[Dict]) -> None: """ Safely updates current environment state in shared state. """ with self.state.lock: self.state.recipe["current_environment_state"] = value self.set_desired_sensor_values(value) # type: ignore ##### STATE MACHINE FUNCTIONS ###################################################### def run(self) -> None: """Runs state machine.""" # Loop forever while True: # Check if thread is shutdown if self.is_shutdown: break # Check for mode transitions if self.mode == modes.INIT: self.run_init_mode() if self.mode == modes.NORECIPE: self.run_norecipe_mode() elif self.mode == modes.START: self.run_start_mode() elif self.mode == modes.QUEUED: self.run_queued_mode() elif self.mode == modes.NORMAL: self.run_normal_mode() elif self.mode == modes.PAUSE: self.run_pause_mode() elif self.mode == modes.STOP: self.run_stop_mode() elif self.mode == modes.RESET: self.run_reset_mode() elif self.mode == modes.ERROR: self.run_error_mode() elif self.mode == modes.SHUTDOWN: self.run_shutdown_mode() else: self.logger.critical("Invalid state machine mode") self.mode = modes.INVALID self.is_shutdown = True break def run_init_mode(self) -> None: """Runs initialization mode. Checks for stored recipe mode and transitions to mode if exists, else transitions to no recipe mode.""" self.logger.info("Entered INIT") # Initialize state self.is_active = False # Check for stored mode mode = self.state.recipe.get("stored_mode") if mode != None: self.logger.debug("Returning to stored mode: {}".format(mode)) self.mode = mode # type: ignore else: self.mode = modes.NORECIPE def run_norecipe_mode(self) -> None: """Runs no recipe mode. Clears recipe and desired sensor state then waits for new events and transitions.""" self.logger.info("Entered NORECIPE") # Set run state self.is_active = False # Clear state self.clear_recipe_state() self.clear_desired_sensor_state() # Loop forever while True: # Check for events self.check_events() # Check for transitions if self.new_transition(modes.NORECIPE): break # Update every 100ms time.sleep(0.1) def run_start_mode(self) -> None: """Runs start mode. Loads commanded recipe uuid into shared state, retrieves recipe json from recipe table, generates recipe transitions, stores them in the recipe transitions table, extracts recipe duration and start time then transitions to queued mode.""" # Set run state self.is_active = True try: self.logger.info("Entered START") # Get recipe json from recipe uuid recipe_json = models.RecipeModel.objects.get(uuid=self.recipe_uuid).json recipe_dict = json.loads(recipe_json) # Parse recipe transitions transitions = self.parse(recipe_dict) # Store recipe transitions in database self.store_recipe_transitions(transitions) # Set recipe duration self.duration_minutes = transitions[-1]["minute"] # Set recipe name self.recipe_name = recipe_dict["name"] self.logger.info("Started: {}".format(self.recipe_name)) # Transition to queued mode self.mode = modes.QUEUED except Exception as e: message = "Unable to start recipe, unhandled exception {}".format(e) self.logger.critical(message) self.mode = modes.NORECIPE def run_queued_mode(self) -> None: """Runs queued mode. Waits for recipe start timestamp to be greater than or equal to current timestamp then transitions to NORMAL.""" self.logger.info("Entered QUEUED") # Set state self.is_active = True # Initialize time counter prev_time_seconds = 0.0 # Loop forever while True: # Check if recipe is ready to run current = self.current_timestamp_minutes start = self.start_timestamp_minutes if current >= start: # type: ignore self.mode = modes.NORMAL break # Calculate remaining delay time delay_minutes = start - current # type: ignore # Log remaining delay time every hour if remaining time > 1 hour if delay_minutes > 60 and time.time() > prev_time_seconds + 3600: prev_time_seconds = time.time() delay_hours = int(delay_minutes / 60.0) self.logger.debug("Starting recipe in {} hours".format(delay_hours)) # Log remaining delay time every minute if remaining time < 1 hour elif delay_minutes < 60 and time.time() > prev_time_seconds + 60: prev_time_seconds = time.time() self.logger.debug("Starting recipe in {} minutes".format(delay_minutes)) # Check for events self.check_events() # Check for transitions if self.new_transition(modes.QUEUED): break # Update every 100ms time.sleep(0.1) def run_normal_mode(self) -> None: """ Runs normal mode. Updates recipe and environment states every minute. Checks for events and transitions.""" self.logger.info("Entered NORMAL") # Set state self.is_active = True # Update recipe environment on first entry self.update_recipe_environment() # Loop forever while True: # Update recipe and environment states every minute if self.new_minute(): self.update_recipe_environment() # Check for recipe end if self.current_phase == "End" and self.current_cycle == "End": self.logger.info("Recipe is over, so transitions from NORMAL to STOP") self.mode = modes.STOP break # Check for events self.check_events() # Check for transitions if self.new_transition(modes.NORMAL): break # Update every 100ms time.sleep(0.1) def run_pause_mode(self) -> None: """Runs pause mode. Clears recipe and desired sensor state, waits for new events and transitions.""" self.logger.info("Entered PAUSE") # Set state self.is_active = True # Clear recipe and desired sensor state self.clear_recipe_state() self.clear_desired_sensor_state() # Loop forever while True: # Check for events self.check_events() # Check for transitions if self.new_transition(modes.PAUSE): break # Update every 100ms time.sleep(0.1) def run_stop_mode(self) -> None: """Runs stop mode. Clears recipe and desired sensor state then transitions to no recipe mode.""" self.logger.info("Entered STOP") # Clear recipe and desired sensor states self.clear_recipe_state() self.clear_desired_sensor_state() # Set state self.is_active = False # Transition to NORECIPE self.mode = modes.NORECIPE def run_error_mode(self) -> None: """Runs error mode. Clears recipe state and desired sensor state then waits for new events and transitions.""" self.logger.info("Entered ERROR") # Clear recipe and desired sensor states self.clear_recipe_state() self.clear_desired_sensor_state() # Set state self.is_active = False # Loop forever while True: # Check for events self.check_events() # Check for transitions if self.new_transition(modes.ERROR): break # Update every 100ms time.sleep(0.1) def run_reset_mode(self) -> None: """Runs reset mode. Clears error state then transitions to init mode.""" self.logger.info("Entered RESET") # Transition to INIT self.mode = modes.INIT ##### HELPER FUNCTIONS ############################################################# def get_recipe_environment(self, minute: int) -> Any: """Gets environment object from database for provided minute.""" return ( models.RecipeTransitionModel.objects.filter(minute__lte=minute).order_by( "-minute" ).first() ) def store_recipe_transitions(self, recipe_transitions: List) -> None: """Stores recipe transitions in database.""" # Clear recipe transitions table in database models.RecipeTransitionModel.objects.all().delete() # Create recipe transitions entries for transitions in recipe_transitions: models.RecipeTransitionModel.objects.create( minute=transitions["minute"], phase=transitions["phase"], cycle=transitions["cycle"], environment_name=transitions["environment_name"], environment_state=transitions["environment_state"], ) def update_recipe_environment(self) -> None: """ Updates recipe environment. """ self.logger.debug("Updating recipe environment") current = self.current_timestamp_minutes start = self.start_timestamp_minutes self.last_update_minute = current - start # type: ignore environment = self.get_recipe_environment(self.last_update_minute) self.current_phase = environment.phase self.current_cycle = environment.cycle self.current_environment_name = environment.environment_name self.current_environment_state = environment.environment_state def clear_desired_sensor_state(self) -> None: """ Sets desired sensor state to null values. """ with self.state.lock: for variable in self.state.environment["sensor"]["desired"]: self.state.environment["sensor"]["desired"][variable] = None def clear_recipe_state(self) -> None: """Sets recipe state to null values.""" self.recipe_name = None self.recipe_uuid = None self.duration_minutes = None self.last_update_minute = None self.start_timestamp_minutes = None self.current_phase = None self.current_cycle = None self.current_environment_name = None self.current_environment_state = {} self.stored_mode = None def new_minute(self) -> bool: """Check if system clock is on a new minute.""" current = self.current_timestamp_minutes start = self.start_timestamp_minutes current_minute = current - start # type: ignore last_update_minute = self.state.recipe["last_update_minute"] if current_minute > last_update_minute: return True else: return False def get_duration_string(self, duration_minutes: int) -> str: """Converts duration in minutes to duration day-hour-minute string.""" days = int(float(duration_minutes) / (60 * 24)) hours = int((float(duration_minutes) - days * 60 * 24) / 60) minutes = int(duration_minutes - days * 60 * 24 - hours * 60) string = "{} Days {} Hours {} Minutes".format(days, hours, minutes) return string def set_desired_sensor_values(self, environment_dict: Dict) -> None: """Sets desired sensor values from provided environment dict.""" with self.state.lock: for variable in environment_dict: value = environment_dict[variable] self.state.environment["sensor"]["desired"][variable] = value def validate( self, json_: str, should_exist: Optional[bool] = None ) -> Tuple[bool, Optional[str]]: """Validates a recipe. Returns true if valid.""" # Load recipe schema schema = json.load(open(RECIPE_SCHEMA_PATH)) # Check valid json and try to parse recipe try: # Decode json recipe = json.loads(json_) # Validate recipe against schema jsonschema.validate(recipe, schema) # Get top level recipe parameters format_ = recipe["format"] version = recipe["version"] name = recipe["name"] uuid = recipe["uuid"] cultivars = recipe["cultivars"] cultivation_methods = recipe["cultivation_methods"] environments = recipe["environments"] phases = recipe["phases"] except json.decoder.JSONDecodeError as e: message = "Invalid recipe json encoding: {}".format(e) self.logger.debug(message) return False, message except jsonschema.exceptions.ValidationError as e: message = "Invalid recipe json schema: {}".format(e.message) self.logger.debug(message) return False, message except KeyError as e: self.logger.critical("Recipe schema did not ensure `{}` exists".format(e)) message = "Invalid recipe json schema: `{}` is requred".format(e) return False, message except Exception as e: self.logger.critical("Invalid recipe, unhandled exception: {}".format(e)) return False, "Unhandled exception: {}".format(type(e)) # Check valid uuid if uuid == None or len(uuid) == 0: return False, "Invalid uuid" # Check recipe existance criteria, does not check if should_exist == None recipe_exists = models.RecipeModel.objects.filter(uuid=uuid).exists() if should_exist == True and not recipe_exists: return False, "UUID does not exist" elif should_exist == False and recipe_exists: return False, "UUID already exists" # Check cycle environment key names are valid try: for phase in phases: for cycle in phase["cycles"]: cycle_name = cycle["name"] environment_key = cycle["environment"] if environment_key not in environments: message = "Invalid environment key `{}` in cycle `{}`".format( environment_key, cycle_name ) self.logger.debug(message) return False, message except KeyError as e: self.logger.critical("Recipe schema did not ensure `{}` exists".format(e)) message = "Invalid recipe json schema: `{}` is requred".format(e) return False, message # Build list of environment variables env_vars: List[str] = [] for env_key, env_dict in environments.items(): for env_var, _ in env_dict.items(): if env_var != "name" and env_var not in env_vars: env_vars.append(env_var) # Check environment variables are valid sensor variables for env_var in env_vars: if not models.SensorVariableModel.objects.filter(key=env_var).exists(): message = "Invalid recipe environment variable: `{}`".format(env_var) self.logger.debug(message) return False, message """ TODO: Reinstate these checks once cloud system has support for enforcing uniqueness of cultivars and cultivation methods. While we are at it, my as well do the same for variable types so can create "scientific" recipes from the cloud UI and send complete recipes. Cloud system will need a way to manage recipes and recipe derivatives. R.e. populating cultivars table, might just want to scrape seedsavers or leverage another existing organism database. Probably also want to think about organismal groups (i.e. classifications). Classifications could the standard scientific (Kingdom , Phylum, etc.) or a more user-friendly group (e.g. Leafy Greens, Six-Week Grows, Exotic Plants, Pre-Historic Plants, etc.) # Check cultivars are valid for cultivar in cultivars: cultivar_name = cultivar["name"] cultivar_uuid = cultivar["uuid"] if not models.CultivarModel.objects.filter(uuid=cultivar_uuid).exists(): message = "Invalid recipe cultivar: `{}`".format(cultivar_name) self.logger.debug(message) return False, message # Check cultivation methods are valid for method in cultivation_methods: method_name = method["name"] method_uuid = method["uuid"] if not models.CultivationMethodModel.objects.filter( uuid=method_uuid ).exists(): message = "Invalid recipe cultivation method: `{}`".format(method_name) self.logger.debug(message) return False, message """ # Recipe is valid return True, None def parse(self, recipe: Dict[str, Any]) -> List[Dict[str, Any]]: """ Parses recipe into state transitions. """ transitions = [] minute_counter = 0 for phase in recipe["phases"]: phase_name = phase["name"] for i in range(phase["repeat"]): for cycle in phase["cycles"]: # Get environment object and cycle name environment_key = cycle["environment"] environment = recipe["environments"][environment_key] cycle_name = cycle["name"] # Get duration if "duration_hours" in cycle: duration_hours = cycle["duration_hours"] duration_minutes = duration_hours * 60 elif "duration_minutes" in cycle: duration_minutes = cycle["duration_minutes"] else: raise KeyError( "Could not find 'duration_minutes' or 'duration_hours' in cycle" ) # Make shallow copy of env so we can pop a property locally environment_copy = dict(environment) environment_name = environment_copy["name"] del environment_copy["name"] environment_state = environment_copy # Write recipe transition to database transitions.append( { "minute": minute_counter, "phase": phase_name, "cycle": cycle_name, "environment_name": environment_name, "environment_state": environment_state, } ) # Increment minute counter minute_counter += duration_minutes # Set recipe end transitions.append( { "minute": minute_counter, "phase": "End", "cycle": "End", "environment_name": "End", "environment_state": {}, } ) # Return state transitions return transitions def check_events(self) -> None: """Checks for a new event. Only processes one event per call, even if there are multiple in the queue. Events are processed first-in-first-out (FIFO).""" # Check for new events if self.event_queue.empty(): return # Get request request = self.event_queue.get() self.logger.debug("Received new request: {}".format(request)) # Get request parameters try: type_ = request["type"] except KeyError as e: message = "Invalid request parameters: {}".format(e) self.logger.exception(message) return # Execute request if type_ == events.START: self._start_recipe(request) elif type_ == events.STOP: self._stop_recipe() else: self.logger.error("Invalid event request type in queue: {}".format(type_)) def start_recipe( self, uuid: str, timestamp: Optional[float] = None, check_mode: bool = True ) -> Tuple[str, int]: """Adds a start recipe event to event queue.""" self.logger.debug("Adding start recipe event to event queue") self.logger.debug("Recipe UUID: {}, timestamp: {}".format(uuid, timestamp)) # Check recipe uuid exists if not models.RecipeModel.objects.filter(uuid=uuid).exists(): message = "Unable to start recipe, invalid uuid" return message, 400 # Check timestamp is valid if provided if timestamp != None and timestamp < time.time(): # type: ignore message = "Unable to start recipe, timestamp must be in the future" return message, 400 # Check valid mode transition if enabled if check_mode and not self.valid_transition(self.mode, modes.START): message = "Unable to start recipe from {} mode".format(self.mode) self.logger.debug(message) return message, 400 # Add start recipe event request to event queue request = {"type": events.START, "uuid": uuid, "timestamp": timestamp} self.event_queue.put(request) # Successfully added recipe to event queue message = "Starting recipe" return message, 202 def _start_recipe(self, request: Dict[str, Any]) -> None: """Starts a recipe. Assumes request has been verified in public start recipe function.""" self.logger.debug("Starting recipe") # Get request parameters uuid = request.get("uuid") timestamp = request.get("timestamp") # Convert timestamp to minutes if not None if timestamp != None: timestamp_minutes = int(timestamp / 60.0) # type: ignore else: timestamp_minutes = int(time.time() / 60.0) # Check valid mode transition if not self.valid_transition(self.mode, modes.START): self.logger.critical("Tried to start recipe from {} mode".format(self.mode)) return # Start recipe on next state machine update self.recipe_uuid = uuid self.start_timestamp_minutes = timestamp_minutes self.mode = modes.START def stop_recipe(self, check_mode: bool = True) -> Tuple[str, int]: """Adds stop recipe event to event queue.""" self.logger.debug("Adding stop recipe event to event queue") # Check valid mode transition if enabled if check_mode and not self.valid_transition(self.mode, modes.STOP): message = "Unable to stop recipe from {} mode".format(self.mode) self.logger.debug(message) return message, 400 # Put request into queue request = {"type": events.STOP} self.event_queue.put(request) # Successfully added stop recipe to event queue message = "Stopping recipe" return message, 200 def _stop_recipe(self) -> None: """Stops a recipe. Assumes request has been verified in public stop recipe function.""" self.logger.debug("Stopping recipe") # Check valid mode transition if not self.valid_transition(self.mode, modes.STOP): self.logger.critical("Tried to stop recipe from {} mode".format(self.mode)) return # Stop recipe on next state machine update self.mode = modes.STOP def create_recipe(self, json_: str) -> Tuple[str, int]: """Creates a recipe into database.""" self.logger.debug("Creating recipe") # Check if recipe is valid is_valid, error = self.validate(json_, should_exist=False) if not is_valid: message = "Unable to create recipe. {}".format(error) self.logger.debug(message) return message, 400 # Create recipe in database try: recipe = json.loads(json_) models.RecipeModel.objects.create(json=json.dumps(recipe)) message = "Successfully created recipe" return message, 200 except: message = "Unable to create recipe, unhandled exception" self.logger.exception(message) return message, 500 def update_recipe(self, json_: str) -> Tuple[str, int]: """Updates an existing recipe in database.""" # Check if recipe is valid is_valid, error = self.validate(json_, should_exist=False) if not is_valid: message = "Unable to update recipe. {}".format(error) self.logger.debug(message) return message, 400 # Update recipe in database try: recipe = json.loads(json_) r = models.RecipeModel.objects.get(uuid=recipe["uuid"]) r.json = json.dumps(recipe) r.save() message = "Successfully updated recipe" return message, 200 except: message = "Unable to update recipe, unhandled exception" self.logger.exception(message) return message, 500 def create_or_update_recipe(self, json_: str) -> Tuple[str, int]: """Creates or updates an existing recipe in database.""" # Check if recipe is valid is_valid, error = self.validate(json_, should_exist=None) if not is_valid: message = "Unable to create/update recipe -> {}".format(error) return message, 400 # Check if creating or updating recipe in database recipe = json.loads(json_) if not models.RecipeModel.objects.filter(uuid=recipe["uuid"]).exists(): # Create recipe try: recipe = json.loads(json_) models.RecipeModel.objects.create(json=json.dumps(recipe)) message = "Successfully created recipe" return message, 200 except: message = "Unable to create recipe, unhandled exception" self.logger.exception(message) return message, 500 else: # Update recipe try: r = models.RecipeModel.objects.get(uuid=recipe["uuid"]) r.json = json.dumps(recipe) r.save() message = "Successfully updated recipe" return message, 200 except: message = "Unable to update recipe, unhandled exception" self.logger.exception(message) return message, 500 def recipe_exists(self, uuid: str) -> bool: """Checks if a recipe exists.""" return models.RecipeModel.objects.filter(uuid=uuid).exists()
class SHT25Driver: """Driver for sht25 temperature and humidity sensor.""" # Initialize variable properties min_temperature = -40 # celcius max_temperature = 125 # celcius min_humidity = 0 # %RH max_humidity = 100 # %RH def __init__( self, name: str, i2c_lock: threading.Lock, bus: int, address: int, mux: Optional[int] = None, channel: Optional[int] = None, simulate: Optional[bool] = False, mux_simulator: Optional[MuxSimulator] = None, ) -> None: """Initializes driver.""" # Initialize logger self.logger = Logger(name="Driver({})".format(name), dunder_name=__name__) # Check if simulating if simulate: self.logger.info("Simulating driver") Simulator = SHT25Simulator else: Simulator = None # Initialize I2C try: self.i2c = I2C( name=name, i2c_lock=i2c_lock, bus=bus, address=address, mux=mux, channel=channel, mux_simulator=mux_simulator, PeripheralSimulator=Simulator, verify_device=False, # need to write before device responds to read ) self.read_user_register(retry=True) except I2CError as e: raise InitError(logger=self.logger) from e def read_temperature(self, retry: bool = True) -> Optional[float]: """ Reads temperature value.""" self.logger.debug("Reading temperature") # Send read temperature command (no-hold master) try: self.i2c.write(bytes([0xF3]), retry=retry) except I2CError as e: raise ReadTemperatureError(logger=self.logger) from e # Wait for sensor to process, see datasheet Table 7 # SHT25 is 12-bit so max temperature processing time is 22ms time.sleep(0.22) # Read sensor data try: bytes_ = self.i2c.read(2, retry=retry) except I2CError as e: raise ReadTemperatureError(logger=self.logger) from e # Convert temperature data and set significant figures msb, lsb = bytes_ raw = msb * 256 + lsb temperature = float(-46.85 + ((raw * 175.72) / 65536.0)) temperature = float("{:.0f}".format(temperature)) # Verify temperature value within valid range if temperature > self.min_temperature and temperature < self.min_temperature: self.logger.warning("Temperature outside of valid range") return None # Successfully read temperature self.logger.debug("Temperature: {} C".format(temperature)) return temperature def read_humidity(self, retry: bool = True) -> Optional[float]: """Reads humidity value.""" self.logger.debug("Reading humidity value from hardware") # Send read humidity command (no-hold master) try: self.i2c.write(bytes([0xF5]), retry=retry) except I2CError as e: raise ReadHumidityError(logger=self.logger) from e # Wait for sensor to process, see datasheet Table 7 # SHT25 is 12-bit so max humidity processing time is 29ms time.sleep(0.29) # Read sensor try: bytes_ = self.i2c.read(2, retry=retry) # Read sensor data except I2CError as e: raise ReadHumidityError(logger=self.logger) from e # Convert humidity data and set significant figures msb, lsb = bytes_ raw = msb * 256 + lsb humidity = float(-6 + ((raw * 125.0) / 65536.0)) humidity = float("{:.0f}".format(humidity)) # Verify humidity value within valid range if humidity > self.min_humidity and humidity < self.min_humidity: self.logger.warning("Humidity outside of valid range") return None # Successfully read humidity self.logger.debug("Humidity: {} %".format(humidity)) return humidity def read_user_register(self, retry: bool = True) -> UserRegister: """ Reads user register.""" self.logger.debug("Reading user register") # Read register try: byte = self.i2c.read_register(0xE7, retry=retry) except I2CError as e: raise ReadUserRegisterError(logger=self.logger) from e # Parse register content resolution_msb = bitwise.get_bit_from_byte(bit=7, byte=byte) resolution_lsb = bitwise.get_bit_from_byte(bit=0, byte=byte) user_register = UserRegister( resolution=resolution_msb << 1 + resolution_lsb, end_of_battery=bool(bitwise.get_bit_from_byte(bit=6, byte=byte)), heater_enabled=bool(bitwise.get_bit_from_byte(bit=2, byte=byte)), reload_disabled=bool(bitwise.get_bit_from_byte(bit=1, byte=byte)), ) # Successfully read user register self.logger.debug("User register: {}".format(user_register)) return user_register def reset(self, retry: bool = True) -> None: """Initiates soft reset.""" self.logger.info("Initiating soft reset") # Send reset command try: self.i2c.write(bytes([0xFE]), retry=retry) except I2CError as e: raise ResetError(logger=self.logger) from e
class NetworkUtility(ABC): """Abstract class that defines shared and required methods for all concrete NetworkUtility classes""" def __init__(self) -> None: self.logger = Logger("NetworkUtility", "network") def is_connected(self) -> bool: """Shared method to determine if the device can reach the network""" try: urllib.request.urlopen("https://google.com") return True except urllib.error.URLError as e: # type: ignore self.logger.debug("Network is not connected: {}".format(e)) return False def get_ip_address(self) -> str: """Shared method to determine the device's IP address""" self.logger.debug("Getting IP Address") try: s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) s.connect(("8.8.8.8", 80)) ip = str(s.getsockname()[0]) s.close() return ip except Exception as e: message = "Unable to get ip address, unhandled exception: {}".format( type(e) ) self.logger.exception(message) return "Unknown" @abstractmethod def get_wifi_ssids( self, exclude_hidden: bool = True, exclude_beaglebones: bool = True ) -> List[Dict[str, str]]: """Abstract method to get wifi SSIDs for configuration""" self.logger.debug("Generic Network Util, can't get SSIDs") return [] @abstractmethod def join_wifi(self, ssid: str, password: str) -> None: """Abstract method to join a wifi network with just a password""" self.logger.debug("join_wifi_advance not implemented") pass @abstractmethod def join_wifi_advanced( self, ssid_name: str, passphrase: str, hidden_ssid: str, security: str, eap: str, identity: str, phase2: str, ) -> None: """Abstract method to join a wifi network with advanced options""" self.logger.debug("join_wifi_advance not implemented") pass @abstractmethod def delete_wifis(self) -> None: """Abstract method to forget all known wifi connections""" self.logger.debug("delete_wifis not implemented") raise SystemError("System does not support deleteing WiFis")
class AtlasDriver: """Parent class for atlas drivers.""" def __init__( self, name: str, i2c_lock: threading.RLock, bus: int, address: int, mux: Optional[int] = None, channel: Optional[int] = None, simulate: bool = False, mux_simulator: Optional[MuxSimulator] = None, Simulator: Optional[PeripheralSimulator] = None, ) -> None: """ Initializes atlas driver. """ # Initialize parameters self.simulate = simulate # Initialize logger logname = "Driver({})".format(name) self.logger = Logger(logname, "peripherals") # Initialize I2C try: self.i2c = I2C( name=name, i2c_lock=i2c_lock, bus=bus, address=address, mux=mux, channel=channel, mux_simulator=mux_simulator, PeripheralSimulator=Simulator, ) except I2CError as e: raise exceptions.InitError(logger=self.logger) def setup(self, retry: bool = True) -> None: """Setsup sensor.""" self.logger.debug("Setting up sensor") try: self.enable_led() info = self.read_info() if info.firmware_version > 1.94: self.enable_protocol_lock() except Exception as e: raise exceptions.SetupError(logger=self.logger) from e def process_command( self, command_string: str, process_seconds: float, num_bytes: int = 31, retry: bool = True, read_response: bool = True, ) -> Optional[str]: """Sends command string to device, waits for processing seconds, then tries to read num response bytes with optional retry if device returns a `still processing` response code. Read retry is enabled by default. Returns response string on success or raises exception on error.""" self.logger.debug("Processing command: {}".format(command_string)) try: # Send command to device byte_array = bytearray(command_string + "\00", "utf8") self.i2c.write(bytes(byte_array), retry=retry) # Check if reading response if read_response: return self.read_response(process_seconds, num_bytes, retry=retry) # Otherwise return none return None except Exception as e: raise exceptions.ProcessCommandError(logger=self.logger) from e def read_response(self, process_seconds: float, num_bytes: int, retry: bool = True) -> str: """Reads response from from device. Waits processing seconds then tries to read num response bytes with optional retry. Returns response string on success or raises exception on error.""" # Give device time to process self.logger.debug("Waiting for {} seconds".format(process_seconds)) time.sleep(process_seconds) # Read device dataSet try: self.logger.debug("Reading response") data = self.i2c.read(num_bytes) except Exception as e: raise exceptions.ReadResponseError(logger=self.logger) from e # Format response code response_code = int(data[0]) # Check for invalid syntax if response_code == 2: message = "invalid command string syntax" raise exceptions.ReadResponseError(message=message, logger=self.logger) # Check if still processing elif response_code == 254: # Try to read one more time if retry enabled if retry == True: self.logger.debug("Sensor still processing, retrying read") return self.read_response(process_seconds, num_bytes, retry=False) else: message = "insufficient processing time" raise exceptions.ReadResponseError(message, logger=self.logger) # Check if device has no data to send elif response_code == 255: # Try to read one more time if retry enabled if retry == True: self.logger.warning( "Sensor reported no data to read, retrying read") return self.read_response(process_seconds, num_bytes, retry=False) else: message = "insufficient processing time" raise exceptions.ReadResponseError(message=message, logger=self.logger) # Invalid response code elif response_code != 1: message = "invalid response code" raise exceptions.ReadResponseError(message=message, logger=self.logger) # Successfully read response response_message = str(data[1:].decode("utf-8").strip("\x00")) self.logger.debug("Response:`{}`".format(response_message)) return response_message def read_info(self, retry: bool = True) -> Info: """Read sensor info register containing sensor type and firmware version. e.g. EC, 2.0.""" self.logger.debug("Reading info register") # Send command try: response = self.process_command("i", process_seconds=0.3, retry=retry) except Exception as e: raise exceptions.ReadInfoError(logger=self.logger) from e # Parse response _, sensor_type, firmware_version = response.split(",") # type: ignore firmware_version = float(firmware_version) # Store firmware version self.firmware_version = firmware_version # Create info dataclass info = Info(sensor_type=sensor_type.lower(), firmware_version=firmware_version) # Successfully read info self.logger.debug(str(info)) return info def read_status(self, retry: bool = True) -> Status: """Reads status from device.""" self.logger.debug("Reading status register") try: response = self.process_command("Status", process_seconds=0.3, retry=retry) except Exception as e: raise exceptions.ReadStatusError(logger=self.logger) from e # Parse response message command, code, voltage = response.split(",") # type: ignore # Break out restart code if code == "P": prev_restart_reason = "Powered off" self.logger.debug("Device previous restart due to powered off") elif code == "S": prev_restart_reason = "Software reset" self.logger.debug("Device previous restart due to software reset") elif code == "B": prev_restart_reason = "Browned out" self.logger.critical("Device browned out on previous restart") elif code == "W": prev_restart_reason = "Watchdog" self.logger.debug("Device previous restart due to watchdog") elif code == "U": self.prev_restart_reason = "Unknown" self.logger.warning("Device previous restart due to unknown") # Build status data class status = Status(prev_restart_reason=prev_restart_reason, voltage=float(voltage)) # Successfully read status self.logger.debug(str(status)) return status def enable_protocol_lock(self, retry: bool = True) -> None: """Enables protocol lock.""" self.logger.debug("Enabling protocol lock") try: self.process_command("Plock,1", process_seconds=0.9, retry=retry) except Exception as e: raise exceptions.EnableProtocolLockError(logger=self.logger) from e def disable_protocol_lock(self, retry: bool = True) -> None: """Disables protocol lock.""" self.logger.debug("Disabling protocol lock") try: self.process_command("Plock,0", process_seconds=0.9, retry=retry) except Exception as e: raise exceptions.DisableProtocolLockError( logger=self.logger) from e def enable_led(self, retry: bool = True) -> None: """Enables led.""" self.logger.debug("Enabling led") try: self.process_command("L,1", process_seconds=1.8, retry=retry) except Exception as e: raise exceptions.EnableLEDError(logger=self.logger) from e def disable_led(self, retry: bool = True) -> None: """Disables led.""" self.logger.debug("Disabling led") try: self.process_command("L,0", process_seconds=1.8, retry=retry) except Exception as e: raise exceptions.DisableLEDError(logger=self.logger) from e def enable_sleep_mode(self, retry: bool = True) -> None: """Enables sleep mode, sensor will wake up by sending any command to it.""" self.logger.debug("Enabling sleep mode") # Send command try: self.process_command("Sleep", process_seconds=0.3, read_response=False, retry=retry) except Exception as e: raise exceptions.EnableSleepModeError(logger=self.logger) from e def set_compensation_temperature(self, temperature: float, retry: bool = True) -> None: """Sets compensation temperature.""" self.logger.debug("Setting compensation temperature") try: command = "T,{}".format(temperature) self.process_command(command, process_seconds=0.3, retry=retry) except Exception as e: raise exceptions.SetCompensationTemperatureError( logger=self.logger) from e def calibrate_low(self, value: float, retry: bool = True) -> None: """Takes a low point calibration reading.""" self.logger.debug("Taking low point calibration reading") try: command = "Cal,low,{}".format(value) self.process_command(command, process_seconds=0.9, retry=retry) except Exception as e: raise exceptions.TakeLowPointCalibrationError( logger=self.logger) from e def calibrate_mid(self, value: float, retry: bool = True) -> None: """Takes a mid point calibration reading.""" self.logger.debug("Taking mid point calibration reading") try: command = "Cal,mid,{}".format(value) self.process_command(command, process_seconds=0.9, retry=retry) except Exception as e: raise exceptions.TakeMidPointCalibrationError( logger=self.logger) from e def calibrate_high(self, value: float, retry: bool = True) -> None: """Takes a high point calibration reading.""" self.logger.debug("Taking high point calibration reading") try: command = "Cal,high,{}".format(value) self.process_command(command, process_seconds=0.9, retry=retry) except Exception as e: raise exceptions.TakeHighPointCalibrationError( logger=self.logger) from e def clear_calibrations(self, retry: bool = True) -> None: """Clears calibration readings.""" self.logger.debug("Clearing calibration readings") try: self.process_command("Cal,clear", process_seconds=0.9, retry=retry) except Exception as e: raise exceptions.ClearCalibrationError(logger=self.logger) from e def factory_reset(self, retry: bool = True) -> None: """Resets sensor to factory config.""" self.logger.debug("Performing factory reset") try: self.process_command("Factory", process_seconds=0.3, read_response=False, retry=retry) except Exception as e: raise exceptions.FactoryResetError(logger=self.logger) from e
class DeviceIO(object): """Manages byte-level device IO.""" def __init__(self, name: str) -> None: # Initialize parameters self.name = name # Initialize logger logname = "DeviceIO({})".format(name) self.logger = Logger(logname, __name__) # Verify io exists self.logger.debug("Verifying io stream exists") UART.setup("UART1") self.ser = serial.Serial(port = "/dev/ttyO1", baudrate=9600) self.open() self.close() def __del__(self) -> None: """Clean up any resources used by the I2c instance.""" self.close() def __enter__(self) -> object: """Context manager enter function.""" return self def __exit__(self, exc_type: ET, exc_val: EV, exc_tb: EB) -> bool: """Context manager exit function, ensures resources are cleaned up.""" self.close() return False # Don't suppress exceptions def open(self) -> None: """Opens io stream.""" try: self.ser.open() except Exception as e: message = "Unable to open device io: {}".format(device_name) raise InitError(message, logger=self.logger) from e def close(self) -> None: """Closes io stream.""" try: self.ser.close() except: self.logger.exception("Unable to close") @manage_io def write_output(self, pin: int, val: string) -> None: """This tells the Arduino to set a pin to a value""" try: self.send_to_ardunio("*w_{}_{}^".format(pin, val)) except Exception as e: message = "Unable to write {}: write_output(pin={}, val={})".format(self.name, pin, val)) raise WriteError(message) from e @manage_io def read_register( self, address: string, register: string ) -> string: """This informs the Arduino to read an I2C chip, and reports the value back""" try: resp = self.request_to_ardunio("*r_{}_{}^".format(address, register)) return resp except Exception as e: message = "Unable to write {}: write_output(pin={}, val={})".format(self.name, pin, val)) raise WriteError(message) from e
class I2C(object): """I2C communication device. Can communicate with device directly or via an I2C mux.""" def __init__( self, name: str, i2c_lock: threading.RLock, address: int, bus: Optional[int] = None, mux: Optional[int] = None, channel: Optional[int] = None, mux_simulator: Optional[MuxSimulator] = None, PeripheralSimulator: Optional[PeripheralSimulator] = None, verify_device: bool = True, ) -> None: # Initialize passed in parameters self.name = name self.i2c_lock = i2c_lock self.bus = bus self.address = address self.mux = mux self.channel = channel # Initialize logger logname = "I2C({})".format(self.name) self.logger = Logger(logname, "i2c") self.logger.debug("Initializing communication") # Verify mux config if self.mux != None and self.channel == None: raise InitError( "Mux requires channel value to be set") from ValueError # Initialize io if PeripheralSimulator != None: self.logger.debug("Using simulated io stream") self.io = PeripheralSimulator( # type: ignore name, bus, address, mux, channel, mux_simulator) else: self.logger.debug("Using device io stream") with self.i2c_lock: self.io = DeviceIO(name, bus) # Verify mux exists if self.mux != None: self.verify_mux() # Verify device exists if verify_device: self.verify_device() # Successfully initialized! self.logger.debug("Initialization successful") def verify_mux(self) -> None: """Verifies mux exists by trying to set it to a channel.""" try: self.logger.debug("Verifying mux exists") byte = self.set_mux(self.mux, self.channel, retry=True) except MuxError as e: message = "Unable to verify mux exists" raise InitError(message, logger=self.logger) from e def verify_device(self) -> None: """Verifies device exists by trying to read a byte from it.""" try: self.logger.debug("Verifying device exists") byte = self.read(1, retry=True) except ReadError as e: message = "Unable to verify device exists, read error" raise InitError(message, logger=self.logger) from e except MuxError as e: message = "Unable to verify device exists, mux error" raise InitError(message, logger=self.logger) from e @retry((WriteError, MuxError), tries=5, delay=0.2, backoff=3) def write(self, bytes_: bytes, retry: bool = True, disable_mux: bool = False) -> None: """Writes byte list to device. Converts byte list to byte array then sends bytes. Returns error message.""" with self.i2c_lock: self.manage_mux("write bytes", disable_mux) self.logger.debug("Writing bytes: {}".format(byte_str(bytes_))) self.io.write(self.address, bytes_) @retry((ReadError, MuxError), tries=5, delay=0.2, backoff=3) def read(self, num_bytes: int, retry: bool = True, disable_mux: bool = False) -> bytes: """Reads num bytes from device. Returns byte array.""" with self.i2c_lock: self.manage_mux("read bytes", disable_mux) self.logger.debug("Reading {} bytes".format(num_bytes)) bytes_ = bytes(self.io.read(self.address, num_bytes)) self.logger.debug("Read bytes: {}".format(byte_str(bytes_))) return bytes_ @retry((ReadError, MuxError), tries=5, delay=0.2, backoff=3) def read_register(self, register: int, retry: bool = True, disable_mux: bool = False) -> int: """Reads byte stored in register at address.""" with self.i2c_lock: self.manage_mux("read register", disable_mux) self.logger.debug("Reading register: 0x{:02X}".format(register)) return int(self.io.read_register(self.address, register)) @retry((WriteError, MuxError), tries=5, delay=0.2, backoff=3) def write_register(self, register: int, value: int, retry: bool = True, disable_mux: bool = False) -> None: with self.i2c_lock: self.manage_mux("write register", disable_mux) message = "Writing register: 0x{:02X}, value: 0x{:02X}".format( register, value) self.logger.debug(message) self.io.write_register(self.address, register, value) @retry(MuxError, tries=5, delay=0.2, backoff=3) def set_mux(self, mux: int, channel: int, retry: bool = True) -> None: """Sets mux to channel""" with self.i2c_lock: channel_byte = 0x01 << channel self.logger.debug( "Setting mux 0x{:02X} to channel {}, writing: [0x{:02X}]". format(mux, channel, channel_byte)) try: self.io.write(mux, bytes([channel_byte])) except WriteError as e: raise MuxError("Unable to set mux", logger=self.logger) from e def manage_mux(self, message: str, disable_mux: bool) -> None: """Sets mux if enabled.""" if disable_mux: return elif self.mux != None: self.logger.debug("Managing mux to {}".format(message)) self.set_mux(self.mux, self.channel, retry=False)
class T6713Driver: """Driver for t6713 co2 sensor.""" # Initialize co2 properties min_co2 = 10 # ppm max_co2 = 5000 # ppm warmup_timeout = 120 # seconds def __init__( self, name: str, i2c_lock: threading.Lock, bus: int, address: int, mux: Optional[int] = None, channel: Optional[int] = None, simulate: Optional[bool] = False, mux_simulator: Optional[MuxSimulator] = None, ) -> None: """Initializes t6713 driver.""" # Initialize parameters self.simulate = simulate self.i2c_lock = i2c_lock # Initialize logger self.logger = Logger(name="Driver({})".format(name), dunder_name=__name__) # Check if simulating if simulate: self.logger.info("Simulating driver") Simulator = T6713Simulator else: Simulator = None # Initialize I2C try: self.i2c = I2C( name=name, i2c_lock=i2c_lock, bus=bus, address=address, mux=mux, channel=channel, mux_simulator=mux_simulator, PeripheralSimulator=Simulator, ) except I2CError as e: raise InitError(logger=self.logger) from e def setup(self, retry: bool = True) -> None: """Sets up sensor.""" # Set ABC logic state try: self.enable_abc_logic() except EnableABCLogicError as e: raise SetupError(logger=self.logger) from e # Wait at least 2 minutes for sensor to stabilize start_time = time.time() while time.time() - start_time < 120: # Keep logs active self.logger.info("Warming up, waiting for 2 minutes") # Update every few seconds time.sleep(3) # Break out if simulating if self.simulate: break # Wait for sensor to report exiting warm up mode start_time = time.time() while True: # Keep logs active self.logger.info("Warming up, waiting for status") # Read status try: status = self.read_status() except ReadStatusError as e: raise SetupError(logger=self.logger) from e # Check if sensor completed warm up mode if not status.warm_up_mode: self.logger.info("Warmup complete") break # Check if timed out if time.time() - start_time > self.warmup_timeout: raise SetupError("Warmup period timed out", logger=self.logger) # Update every 3 seconds time.sleep(3) def read_co2(self, retry: bool = True) -> Optional[float]: """Reads co2 value.""" self.logger.debug("Reading co2") # Read co2 data, requires mux disable to read all x4 bytes try: with self.i2c_lock: self.i2c.write(bytes([0x04, 0x13, 0x8b, 0x00, 0x01]), retry=retry) bytes_ = self.i2c.read(4, retry=retry, disable_mux=True) except I2CError as e: raise ReadCo2Error(logger=self.logger) from e # Convert co2 data and set significant figures _, _, msb, lsb = bytes_ co2 = float(msb * 256 + lsb) co2 = round(co2, 0) # Verify co2 value within valid range if co2 > self.min_co2 and co2 < self.min_co2: self.logger.warning("Co2 outside of valid range") return None # Successfully read carbon dioxide self.logger.debug("Co2: {} ppm".format(co2)) return co2 def read_status(self, retry: bool = True) -> Status: """Reads status.""" self.logger.debug("Reading status") # Read status data, requires mux diable to read all x4 bytes try: with self.i2c_lock: self.i2c.write(bytes([0x04, 0x13, 0x8a, 0x00, 0x01]), retry=retry) bytes_ = self.i2c.read(4, retry=retry, disable_mux=True) except I2CError as e: raise ReadStatusError(logger=self.logger) from e # Parse status bytes _, _, status_msb, status_lsb = bytes_ status = Status( error_condition=bool(bitwise.get_bit_from_byte(0, status_lsb)), flash_error=bool(bitwise.get_bit_from_byte(1, status_lsb)), calibration_error=bool(bitwise.get_bit_from_byte(2, status_lsb)), rs232=bool(bitwise.get_bit_from_byte(0, status_msb)), rs485=bool(bitwise.get_bit_from_byte(1, status_msb)), i2c=bool(bitwise.get_bit_from_byte(2, status_msb)), warm_up_mode=bool(bitwise.get_bit_from_byte(3, status_msb)), single_point_calibration=bool( bitwise.get_bit_from_byte(7, status_msb)), ) # Successfully read status self.logger.debug("Status: {}".format(status)) return status def enable_abc_logic(self, retry: bool = True) -> None: """Enables ABC logic.""" self.logger.info("Enabling abc logic") try: self.i2c.write(bytes([0x05, 0x03, 0xEE, 0xFF, 0x00]), retry=retry) except I2CError as e: raise EnableABCLogicError(logger=self.logger) from e def disable_abc_logic(self, retry: bool = True) -> None: """Disables ABC logic.""" self.logger.info("Disabling abc logic") try: self.i2c.write(bytes([0x05, 0x03, 0xEE, 0x00, 0x00]), retry=retry) except I2CError as e: raise DisableABCLogicError(logger=self.logger) from e def reset(self, retry: bool = True) -> None: """Initiates soft reset.""" self.logger.info("Performing soft reset") try: self.i2c.write(bytes([0x05, 0x03, 0xE8, 0xFF, 0x00]), retry=retry) except I2CError as e: raise ResetError(logger=self.logger) from e
class CoordinatorManager(StateMachineManager): """Manages device state machine thread that spawns child threads to run recipes, read sensors, set actuators, manage control loops, sync data, and manage external events.""" # Initialize vars latest_publish_timestamp = 0.0 peripherals: Dict[str, StateMachineManager] = {} controllers: Dict[str, StateMachineManager] = {} new_config: bool = False def __init__(self) -> None: """Initializes coordinator.""" # Initialize parent class super().__init__() # Initialize logger self.logger = Logger("Coordinator", "coordinator") self.logger.debug("Initializing coordinator") # Initialize state self.state = State() # Initialize environment state dict, TODO: remove this self.state.environment = { "sensor": { "desired": {}, "reported": {} }, "actuator": { "desired": {}, "reported": {} }, "reported_sensor_stats": { "individual": { "instantaneous": {}, "average": {} }, "group": { "instantaneous": {}, "average": {} }, }, } # Initialize recipe state dict, TODO: remove this self.state.recipe = { "recipe_uuid": None, "start_timestamp_minutes": None, "last_update_minute": None, } # Initialize managers self.recipe = RecipeManager(self.state) self.iot = IotManager(self.state, self.recipe) # type: ignore self.recipe.set_iot(self.iot) self.resource = ResourceManager(self.state, self.iot) # type: ignore self.network = NetworkManager(self.state) # type: ignore self.upgrade = UpgradeManager(self.state) # type: ignore # Initialize state machine transitions self.transitions = { modes.INIT: [modes.CONFIG, modes.ERROR, modes.SHUTDOWN], modes.CONFIG: [modes.SETUP, modes.ERROR, modes.SHUTDOWN], modes.SETUP: [modes.NORMAL, modes.ERROR, modes.SHUTDOWN], modes.NORMAL: [modes.LOAD, modes.ERROR, modes.SHUTDOWN], modes.LOAD: [modes.CONFIG, modes.ERROR, modes.SHUTDOWN], modes.RESET: [modes.INIT, modes.SHUTDOWN], modes.ERROR: [modes.RESET, modes.SHUTDOWN], } # Initialize state machine mode self.mode = modes.INIT @property def mode(self) -> str: """Gets mode.""" return self._mode @mode.setter def mode(self, value: str) -> None: """Safely updates mode in state object.""" self._mode = value with self.state.lock: self.state.device["mode"] = value @property def config_uuid(self) -> Optional[str]: """ Gets config uuid from shared state. """ return self.state.device.get("config_uuid") # type: ignore @config_uuid.setter def config_uuid(self, value: Optional[str]) -> None: """ Safely updates config uuid in state. """ with self.state.lock: self.state.device["config_uuid"] = value @property def config_dict(self) -> Dict[str, Any]: """Gets config dict for config uuid in device config table.""" if self.config_uuid == None: return {} config = models.DeviceConfigModel.objects.get(uuid=self.config_uuid) return json.loads(config.json) # type: ignore @property def latest_environment_timestamp(self) -> float: """Gets latest environment timestamp from environment table.""" if not models.EnvironmentModel.objects.all(): return 0.0 else: environment = models.EnvironmentModel.objects.latest() return float(environment.timestamp.timestamp()) @property def manager_modes(self) -> Dict[str, str]: """Gets manager modes.""" self.logger.debug("Getting manager modes") # Get known manager modes modes = { "Coordinator": self.mode, "Recipe": self.recipe.mode, "Network": self.network.mode, "IoT": self.iot.mode, "Resource": self.resource.mode, } # Get peripheral manager modes for peripheral_name, peripheral_manager in self.peripherals.items(): modes[peripheral_name] = peripheral_manager.mode # Get controller manager modes for controller_name, controller_manager in self.controllers.items(): modes[controller_name] = controller_manager.mode # Return modes self.logger.debug("Returning modes: {}".format(modes)) return modes @property def manager_healths(self) -> Dict[str, str]: """Gets manager healths.""" self.logger.debug("Getting manager healths") # Initialize healths healths = {} # Get peripheral manager modes for peripheral_name, peripheral_manager in self.peripherals.items(): healths[ peripheral_name] = peripheral_manager.health # type: ignore # Return modes self.logger.debug("Returning healths: {}".format(healths)) return healths ##### STATE MACHINE FUNCTIONS ###################################################### def run(self) -> None: """Runs device state machine.""" # Loop forever while True: # Check if thread is shutdown if self.is_shutdown: break # Check for transitions if self.mode == modes.INIT: self.run_init_mode() elif self.mode == modes.CONFIG: self.run_config_mode() elif self.mode == modes.SETUP: self.run_setup_mode() elif self.mode == modes.NORMAL: self.run_normal_mode() elif self.mode == modes.LOAD: self.run_load_mode() elif self.mode == modes.ERROR: self.run_error_mode() elif self.mode == modes.RESET: self.run_reset_mode() elif self.mode == modes.SHUTDOWN: self.run_shutdown_mode() else: self.logger.critical("Invalid state machine mode") self.mode = modes.INVALID self.is_shutdown = True break def run_init_mode(self) -> None: """Runs init mode. Loads local data files and stored database state then transitions to config mode.""" self.logger.info("Entered INIT") # Load local data files and stored db state self.load_local_data_files() self.load_database_stored_state() # Transition to config mode on next state machine update self.mode = modes.CONFIG def run_config_mode(self) -> None: """Runs configuration mode. If device config is not set, loads 'unspecified' config then transitions to setup mode.""" self.logger.info("Entered CONFIG") # Check device config specifier file exists in repo try: with open(DEVICE_CONFIG_PATH) as f: config_name = f.readline().strip() except: env_dev_type = os.getenv("OPEN_AG_DEVICE_TYPE") if env_dev_type is None: config_name = "unspecified" message = "Unable to read {}, using unspecified config".format( DEVICE_CONFIG_PATH) else: config_name = env_dev_type message = "Unable to read {}, using {} config from env".format( DEVICE_CONFIG_PATH, config_name) self.logger.warning(message) # Create the directories if needed os.makedirs(os.path.dirname(DEVICE_CONFIG_PATH), exist_ok=True) # Write `unspecified` to device.txt with open(DEVICE_CONFIG_PATH, "w") as f: f.write("{}\n".format(config_name)) # Load device config self.logger.debug("Loading device config file: {}".format(config_name)) device_config = json.load( open("data/devices/{}.json".format(config_name))) # Check if config uuid changed, if so, adjust state if self.config_uuid != device_config["uuid"]: with self.state.lock: self.state.peripherals = {} self.state.controllers = {} set_nested_dict_safely( self.state.environment, ["reported_sensor_stats"], {}, self.state.lock, ) set_nested_dict_safely(self.state.environment, ["sensor", "reported"], {}, self.state.lock) self.config_uuid = device_config["uuid"] # Transition to setup mode on next state machine update self.mode = modes.SETUP def run_setup_mode(self) -> None: """Runs setup mode. Creates and spawns recipe, peripheral, and controller threads, waits for all threads to initialize then transitions to normal mode.""" self.logger.info("Entered SETUP") config_uuid = self.state.device["config_uuid"] # Spawn managers if not self.new_config: self.recipe.spawn() self.iot.spawn() self.resource.spawn() self.network.spawn() self.upgrade.spawn() # Create and spawn peripherals self.logger.debug("Creating and spawning peripherals") self.create_peripherals() self.spawn_peripherals() # Create and spawn controllers self.create_controllers() self.spawn_controllers() # Wait for all threads to initialize while not self.all_managers_initialized(): time.sleep(0.2) # Unset new config flag self.new_config = False # Transition to normal mode on next state machine update self.mode = modes.NORMAL def run_normal_mode(self) -> None: """Runs normal operation mode. Updates device state summary and stores device state in database, checks for new events and transitions.""" self.logger.info("Entered NORMAL") while True: # Overwrite system state in database every 100ms self.update_state() # Store environment state in every 10 minutes if time.time() - self.latest_environment_timestamp > 60 * 10: self.store_environment() # Check for events self.check_events() # Check for transitions if self.new_transition(modes.NORMAL): break # Update every 100ms time.sleep(0.1) def run_load_mode(self) -> None: """Runs load mode, shutsdown peripheral and controller threads then transitions to config mode.""" self.logger.info("Entered LOAD") # Shutdown peripherals and controllers self.shutdown_peripheral_threads() self.shutdown_controller_threads() # Initialize timeout parameters timeout = 10 start_time = time.time() # Loop forever while True: # Check if peripherals and controllers are shutdown if self.all_peripherals_shutdown( ) and self.all_controllers_shutdown(): self.logger.debug("All peripherals and controllers shutdown") break # Check for timeout if time.time() - start_time > timeout: self.logger.critical("Config threads did not shutdown") self.mode = modes.ERROR return # Update every 100ms time.sleep(0.1) # Set new config flag self.new_config = True # Transition to config mode on next state machine update self.mode = modes.CONFIG def run_reset_mode(self) -> None: """Runs reset mode. Shutsdown child threads then transitions to init.""" self.logger.info("Entered RESET") # Shutdown managers self.shutdown_peripheral_threads() self.shutdown_controller_threads() self.recipe.shutdown() self.iot.shutdown() # Transition to init mode on next state machine update self.mode = modes.INIT def run_error_mode(self) -> None: """Runs error mode. Shutsdown child threads, waits for new events and transitions.""" self.logger.info("Entered ERROR") # Shutsdown peripheral and controller threads self.shutdown_peripheral_threads() self.shutdown_controller_threads() # Loop forever while True: # Check for events self.check_events() # Check for transitions if self.new_transition(modes.ERROR): break # Update every 100ms time.sleep(0.1) ##### SUPPORT FUNCTIONS ############################################################ def update_state(self) -> None: """Updates stored state in database. If state does not exist, creates it.""" # TODO: Move this to state manager if not models.StateModel.objects.filter(pk=1).exists(): models.StateModel.objects.create( id=1, device=json.dumps(self.state.device), recipe=json.dumps(self.state.recipe), environment=json.dumps(self.state.environment), peripherals=json.dumps(self.state.peripherals), controllers=json.dumps(self.state.controllers), iot=json.dumps(self.state.iot), resource=json.dumps(self.state.resource), connect=json.dumps(self.state.network), upgrade=json.dumps(self.state.upgrade), ) else: models.StateModel.objects.filter(pk=1).update( device=json.dumps(self.state.device), recipe=json.dumps(self.state.recipe), environment=json.dumps(self.state.environment), peripherals=json.dumps(self.state.peripherals), controllers=json.dumps(self.state.controllers), iot=json.dumps(self.state.iot), resource=json.dumps(self.state.resource), connect=json.dumps(self.state.network), # TODO: migrate this upgrade=json.dumps(self.state.upgrade), ) def load_local_data_files(self) -> None: """ Loads local data files. """ self.logger.info("Loading local data files") # Load files with no verification dependencies first self.load_sensor_variables_file() self.load_actuator_variables_file() self.load_cultivars_file() self.load_cultivation_methods_file() # Load recipe files after sensor/actuator variables, cultivars, and # cultivation methods since verification depends on them self.load_recipe_files() # Load peripheral setup files after sensor/actuator variable since verification # depends on them self.load_peripheral_setup_files() # Load controller setup files after sensor/actuator variable since verification # depends on them self.load_controller_setup_files() # Load device config after peripheral setups since verification # depends on them self.load_device_config_files() def load_sensor_variables_file(self) -> None: """ Loads sensor variables file into database after removing all existing entries. """ self.logger.debug("Loading sensor variables file") # Load sensor variables and schema sensor_variables = json.load(open(SENSOR_VARIABLES_PATH)) sensor_variables_schema = json.load(open(SENSOR_VARIABLES_SCHEMA_PATH)) # Validate sensor variables with schema jsonschema.validate(sensor_variables, sensor_variables_schema) # Delete sensor variables tables models.SensorVariableModel.objects.all().delete() # Create sensor variables table for sensor_variable in sensor_variables: models.SensorVariableModel.objects.create( json=json.dumps(sensor_variable)) def load_actuator_variables_file(self) -> None: """ Loads actuator variables file into database after removing all existing entries. """ self.logger.debug("Loading actuator variables file") # Load actuator variables and schema actuator_variables = json.load(open(ACTUATOR_VARIABLES_PATH)) actuator_variables_schema = json.load( open(ACTUATOR_VARIABLES_SCHEMA_PATH)) # Validate actuator variables with schema jsonschema.validate(actuator_variables, actuator_variables_schema) # Delete actuator variables tables models.ActuatorVariableModel.objects.all().delete() # Create actuator variables table for actuator_variable in actuator_variables: models.ActuatorVariableModel.objects.create( json=json.dumps(actuator_variable)) def load_cultivars_file(self) -> None: """ Loads cultivars file into database after removing all existing entries.""" self.logger.debug("Loading cultivars file") # Load cultivars and schema cultivars = json.load(open(CULTIVARS_PATH)) cultivars_schema = json.load(open(CULTIVARS_SCHEMA_PATH)) # Validate cultivars with schema jsonschema.validate(cultivars, cultivars_schema) # Delete cultivars tables models.CultivarModel.objects.all().delete() # Create cultivars table for cultivar in cultivars: models.CultivarModel.objects.create(json=json.dumps(cultivar)) def load_cultivation_methods_file(self) -> None: """ Loads cultivation methods file into database after removing all existing entries. """ self.logger.debug("Loading cultivation methods file") # Load cultivation methods and schema cultivation_methods = json.load(open(CULTIVATION_METHODS_PATH)) cultivation_methods_schema = json.load( open(CULTIVATION_METHODS_SCHEMA_PATH)) # Validate cultivation methods with schema jsonschema.validate(cultivation_methods, cultivation_methods_schema) # Delete cultivation methods tables models.CultivationMethodModel.objects.all().delete() # Create cultivation methods table for cultivation_method in cultivation_methods: models.CultivationMethodModel.objects.create( json=json.dumps(cultivation_method)) def load_recipe_files(self) -> None: """Loads recipe files into database via recipe manager create or update function.""" self.logger.debug("Loading recipe files") # Get recipes for filepath in glob.glob(RECIPES_PATH): self.logger.debug("Loading recipe file: {}".format(filepath)) with open(filepath, "r") as f: json_ = f.read().replace("\n", "") message, code = self.recipe.create_or_update_recipe(json_) if code != 200: filename = filepath.split("/")[-1] error = "Unable to load {} -> {}".format(filename, message) self.logger.error(error) def load_peripheral_setup_files(self) -> None: """Loads peripheral setup files from codebase into database by creating new entries after deleting existing entries. Verification depends on sensor and actuator variables.""" self.logger.info("Loading peripheral setup files") # Get peripheral setups peripheral_setups = [] for filepath in glob.glob(PERIPHERAL_SETUP_FILES_PATH): self.logger.debug( "Loading peripheral setup file: {}".format(filepath)) peripheral_setups.append(json.load(open(filepath))) # Get get peripheral setup schema # TODO: Finish schema peripheral_setup_schema = json.load(open(PERIPHERAL_SETUP_SCHEMA_PATH)) # Validate peripheral setups with schema for peripheral_setup in peripheral_setups: jsonschema.validate(peripheral_setup, peripheral_setup_schema) # Delete all peripheral setup entries from database models.PeripheralSetupModel.objects.all().delete() # TODO: Validate peripheral setup variables with database variables # Create peripheral setup entries in database for peripheral_setup in peripheral_setups: models.PeripheralSetupModel.objects.create( json=json.dumps(peripheral_setup)) def load_controller_setup_files(self) -> None: """Loads controller setup files from codebase into database by creating new entries after deleting existing entries. Verification depends on sensor and actuator variables.""" self.logger.info("Loading controller setup files") # Get controller setups controller_setups = [] for filepath in glob.glob(CONTROLLER_SETUP_FILES_PATH): self.logger.debug( "Loading controller setup file: {}".format(filepath)) controller_setups.append(json.load(open(filepath))) # Get get controller setup schema controller_setup_schema = json.load(open(CONTROLLER_SETUP_SCHEMA_PATH)) # Validate peripheral setups with schema for controller_setup in controller_setups: jsonschema.validate(controller_setup, controller_setup_schema) # Delete all peripheral setup entries from database models.ControllerSetupModel.objects.all().delete() # TODO: Validate controller setup variables with database variables # Create peripheral setup entries in database for controller_setup in controller_setups: models.ControllerSetupModel.objects.create( json=json.dumps(controller_setup)) def load_device_config_files(self) -> None: """Loads device config files from codebase into database by creating new entries after deleting existing entries. Verification depends on peripheral setups. """ self.logger.info("Loading device config files") # Get devices device_configs = [] for filepath in glob.glob(DEVICE_CONFIG_FILES_PATH): self.logger.debug( "Loading device config file: {}".format(filepath)) device_configs.append(json.load(open(filepath))) # Get get device config schema # TODO: Finish schema (see optional objects) device_config_schema = json.load(open(DEVICE_CONFIG_SCHEMA_PATH)) # Validate device configs with schema for device_config in device_configs: jsonschema.validate(device_config, device_config_schema) # TODO: Validate device config with peripherals # TODO: Validate device config with varibles # Delete all device config entries from database models.DeviceConfigModel.objects.all().delete() # Create device config entry if new or update existing for device_config in device_configs: models.DeviceConfigModel.objects.create( json=json.dumps(device_config)) def load_database_stored_state(self) -> None: """ Loads stored state from database if it exists. """ self.logger.info("Loading database stored state") # Get stored state from database if not models.StateModel.objects.filter(pk=1).exists(): self.logger.info("No stored state in database") self.config_uuid = None return stored_state = models.StateModel.objects.filter(pk=1).first() # Load device state stored_device_state = json.loads(stored_state.device) # Load recipe state stored_recipe_state = json.loads(stored_state.recipe) self.recipe.recipe_uuid = stored_recipe_state["recipe_uuid"] self.recipe.recipe_name = stored_recipe_state["recipe_name"] self.recipe.duration_minutes = stored_recipe_state["duration_minutes"] self.recipe.start_timestamp_minutes = stored_recipe_state[ "start_timestamp_minutes"] self.recipe.last_update_minute = stored_recipe_state[ "last_update_minute"] self.recipe.stored_mode = stored_recipe_state["mode"] # Load peripherals state stored_peripherals_state = json.loads(stored_state.peripherals) for peripheral_name in stored_peripherals_state: self.state.peripherals[peripheral_name] = {} if "stored" in stored_peripherals_state[peripheral_name]: stored = stored_peripherals_state[peripheral_name]["stored"] self.state.peripherals[peripheral_name]["stored"] = stored # Load controllers state stored_controllers_state = json.loads(stored_state.controllers) for controller_name in stored_controllers_state: self.state.controllers[controller_name] = {} if "stored" in stored_controllers_state[controller_name]: stored = stored_controllers_state[controller_name]["stored"] self.state.controllers[controller_name]["stored"] = stored # Load iot state stored_iot_state = json.loads(stored_state.iot) self.state.iot["stored"] = stored_iot_state.get("stored", {}) def store_environment(self) -> None: """ Stores current environment state in environment table. """ models.EnvironmentModel.objects.create(state=self.state.environment) def create_peripherals(self) -> None: """ Creates peripheral managers. """ self.logger.info("Creating peripheral managers") # Verify peripherals are configured if self.config_dict.get("peripherals") == None: self.logger.info("No peripherals configured") return # Set var type mux_simulator: Optional[MuxSimulator] # Inintilize simulation parameters if os.environ.get("SIMULATE") == "true": simulate = True mux_simulator = MuxSimulator() else: simulate = False mux_simulator = None # Create thread locks i2c_lock = threading.RLock() # Create peripheral managers self.peripherals = {} peripheral_config_dicts = self.config_dict.get("peripherals", {}) for peripheral_config_dict in peripheral_config_dicts: self.logger.debug("Creating {}".format( peripheral_config_dict["name"])) # Get peripheral setup dict peripheral_uuid = peripheral_config_dict["uuid"] peripheral_setup_dict = self.get_peripheral_setup_dict( peripheral_uuid) self.logger.debug("UUID {}".format(peripheral_uuid)) # Verify valid peripheral config dict if peripheral_setup_dict == {}: self.logger.critical( "Invalid peripheral uuid in device " "config. Validator should have caught this.") continue # Get peripheral module and class name module_name = ("device.peripherals.modules." + peripheral_setup_dict["module_name"]) class_name = peripheral_setup_dict["class_name"] # Import peripheral library module_instance = __import__(module_name, fromlist=[class_name]) class_instance = getattr(module_instance, class_name) # Create peripheral manager peripheral_name = peripheral_config_dict["name"] peripheral = class_instance( name=peripheral_name, state=self.state, config=peripheral_config_dict, simulate=simulate, i2c_lock=i2c_lock, mux_simulator=mux_simulator, ) self.peripherals[peripheral_name] = peripheral def get_peripheral_setup_dict(self, uuid: str) -> Dict[str, Any]: """Gets peripheral setup dict for uuid in peripheral setup table.""" if not models.PeripheralSetupModel.objects.filter(uuid=uuid).exists(): return {} else: json_ = models.PeripheralSetupModel.objects.get(uuid=uuid).json peripheral_setup_dict = json.loads(json_) return peripheral_setup_dict # type: ignore def get_controller_setup_dict(self, uuid: str) -> Dict[str, Any]: """Gets controller setup dict for uuid in peripheral setup table.""" if not models.ControllerSetupModel.objects.filter(uuid=uuid).exists(): return {} else: json_ = models.ControllerSetupModel.objects.get(uuid=uuid).json controller_setup_dict = json.loads(json_) return controller_setup_dict # type: ignore def spawn_peripherals(self) -> None: """ Spawns peripherals. """ if self.peripherals == {}: self.logger.info("No peripheral threads to spawn") else: self.logger.info("Spawning peripherals") for name, manager in self.peripherals.items(): manager.spawn() def create_controllers(self) -> None: """ Creates controller managers. """ self.logger.info("Creating controller managers") # Verify controllers are configured if self.config_dict.get("controllers") == None: self.logger.info("No controllers configured") return # Create controller managers self.controllers = {} controller_config_dicts = self.config_dict.get("controllers", {}) for controller_config_dict in controller_config_dicts: self.logger.debug("Creating {}".format( controller_config_dict["name"])) # Get controller setup dict controller_uuid = controller_config_dict["uuid"] controller_setup_dict = self.get_controller_setup_dict( controller_uuid) # Verify valid controller config dict if controller_setup_dict == None: self.logger.critical( "Invalid controller uuid in device " "config. Validator should have caught this.") continue # Get controller module and class name module_name = ("device.controllers.modules." + controller_setup_dict["module_name"]) class_name = controller_setup_dict["class_name"] # Import controller library module_instance = __import__(module_name, fromlist=[class_name]) class_instance = getattr(module_instance, class_name) # Create controller manager controller_name = controller_config_dict["name"] controller_manager = class_instance(controller_name, self.state, controller_config_dict) self.controllers[controller_name] = controller_manager def spawn_controllers(self) -> None: """ Spawns controllers. """ if self.controllers == {}: self.logger.info("No controller threads to spawn") else: self.logger.info("Spawning controllers") for name, manager in self.controllers.items(): self.logger.debug("Spawning {}".format(name)) manager.spawn() def all_managers_initialized(self) -> bool: """Checks if all managers have initialized.""" if self.recipe.mode == modes.INIT: return False elif not self.all_peripherals_initialized(): return False elif not self.all_controllers_initialized(): return False return True def all_peripherals_initialized(self) -> bool: """Checks if all peripherals have initialized.""" for name, manager in self.peripherals.items(): if manager.mode == modes.INIT: return False return True def all_controllers_initialized(self) -> bool: """Checks if all controllers have initialized.""" for name, manager in self.controllers.items(): if manager.mode == modes.INIT: return False return True def shutdown_peripheral_threads(self) -> None: """Shutsdown all peripheral threads.""" for name, manager in self.peripherals.items(): manager.shutdown() def shutdown_controller_threads(self) -> None: """Shutsdown all controller threads.""" for name, manager in self.controllers.items(): manager.shutdown() def all_peripherals_shutdown(self) -> bool: """Check if all peripherals are shutdown.""" for name, manager in self.peripherals.items(): if manager.thread.is_alive(): return False return True def all_controllers_shutdown(self) -> bool: """Check if all controllers are shutdown.""" for name, manager in self.controllers.items(): if manager.thread.is_alive(): return False return True ##### EVENT FUNCTIONS ############################################################## def check_events(self) -> None: """Checks for a new event. Only processes one event per call, even if there are multiple in the queue. Events are processed first-in-first-out (FIFO).""" # Check for new events if self.event_queue.empty(): return # Get request request = self.event_queue.get() self.logger.debug("Received new request: {}".format(request)) # Get request parameters try: type_ = request["type"] except KeyError as e: message = "Invalid request parameters: {}".format(e) self.logger.exception(message) return # Execute request if type_ == events.RESET: self._reset() # Defined in parent class elif type_ == events.SHUTDOWN: self._shutdown() # Defined in parent class elif type_ == events.LOAD_DEVICE_CONFIG: self._load_device_config(request) else: self.logger.error( "Invalid event request type in queue: {}".format(type_)) def load_device_config(self, uuid: str) -> Tuple[str, int]: """Pre-processes load device config event request.""" self.logger.debug("Pre-processing load device config request") # Get filename of corresponding uuid filename = None for filepath in glob.glob(DEVICE_CONFIG_FILES_PATH): self.logger.debug(filepath) device_config = json.load(open(filepath)) if device_config["uuid"] == uuid: filename = filepath.split("/")[-1].replace(".json", "") # Verify valid config uuid if filename == None: message = "Invalid config uuid, corresponding filepath not found" self.logger.debug(message) return message, 400 # Check valid mode transition if enabled if not self.valid_transition(self.mode, modes.LOAD): message = "Unable to load device config from {} mode".format( self.mode) self.logger.debug(message) return message, 400 # Add load device config event request to event queue request = {"type": events.LOAD_DEVICE_CONFIG, "filename": filename} self.event_queue.put(request) # Successfully added load device config request to event queue message = "Loading config" return message, 200 def _load_device_config(self, request: Dict[str, Any]) -> None: """Processes load device config event request.""" self.logger.debug("Processing load device config request") # Get request parameters filename = request.get("filename") # Write config filename to device config path with open(DEVICE_CONFIG_PATH, "w") as f: f.write(str(filename) + "\n") # Transition to init mode on next state machine update self.mode = modes.LOAD
class GroveRGBLCDDriver: """Driver for Grove RGB LCD display.""" # -------------------------------------------------------------------------- # Constants for Grove RBG LCD CMD = 0x80 CLEAR = 0x01 DISPLAY_ON_NO_CURSOR = 0x08 | 0x04 TWO_LINES = 0x28 CHAR = 0x40 NEWLINE = 0xC0 RGB_ADDRESS = 0x62 LCD_ADDRESS = 0x3E # -------------------------------------------------------------------------- def __init__( self, name: str, i2c_lock: threading.RLock, bus: int, rgb_address: int = RGB_ADDRESS, lcd_address: int = LCD_ADDRESS, mux: Optional[int] = None, channel: Optional[int] = None, simulate: bool = False, mux_simulator: Optional[MuxSimulator] = None, ) -> None: """Initializes Grove RGB LCD.""" # Initialize logger logname = "GroveRGBLCD({})".format(name) self.logger = Logger(logname, __name__) # Check if simulating if simulate: self.logger.info("Simulating driver") Simulator = simulator.GroveRGBLCDSimulator else: Simulator = None # Initialize I2C try: self.i2c_rgb = I2C( name="RGB-{}".format(name), i2c_lock=i2c_lock, bus=bus, address=rgb_address, mux=mux, channel=channel, mux_simulator=mux_simulator, PeripheralSimulator=Simulator, ) self.i2c_lcd = I2C( name="LCD-{}".format(name), i2c_lock=i2c_lock, bus=bus, address=lcd_address, mux=mux, channel=channel, mux_simulator=mux_simulator, PeripheralSimulator=Simulator, ) except I2CError as e: raise exceptions.InitError(logger=self.logger) from e # Initialize the display try: # command: clear display self.i2c_lcd.write(bytes([self.CMD, self.CLEAR])) time.sleep(0.05) # Wait for lcd to process # command: display on, no cursor self.i2c_lcd.write(bytes([self.CMD, self.DISPLAY_ON_NO_CURSOR])) # command: 2 lines self.i2c_lcd.write(bytes([self.CMD, self.TWO_LINES])) time.sleep(0.05) # Wait for lcd to process except I2CError as e: raise exceptions.DriverError(logger=self.logger) from e # -------------------------------------------------------------------------- def set_backlight(self, R: int = 0x00, G: int = 0x00, B: int = 0x00) -> None: """Turns on the LCD backlight at the level and color specified. 0 - 255 are valid inputs for RGB.""" # validate the inputs are 0 <> 255 if R < 0 or R > 255 or G < 0 or G > 255 or B < 0 or B > 255: self.logger.error("RGB values must be between 0 and 255") raise exceptions.DriverError(logger=self.logger) message = "Setting RGB backlight: {:2X}, {:2X}, {:2X}".format(R, G, B) self.logger.debug(message) # Set the backlight RGB value try: self.i2c_rgb.write(bytes([0, 0])) self.i2c_rgb.write(bytes([1, 0])) self.i2c_rgb.write(bytes([0x08, 0xAA])) self.i2c_rgb.write(bytes([4, R])) self.i2c_rgb.write(bytes([3, G])) self.i2c_rgb.write(bytes([2, B])) except I2CError as e: raise exceptions.DriverError(logger=self.logger) from e # -------------------------------------------------------------------------- def write_string(self, message: str = "") -> None: """Writes a string to the LCD (16 chars per line limit, 2 lines). Use a '/n' newline character in the string to start the secone line.""" self.logger.debug("Writing '{}' to LCD".format(message)) try: # command: clear display self.i2c_lcd.write(bytes([self.CMD, self.CLEAR])) time.sleep(0.05) # Wait for lcd to process for char in message: # write to the second line? (two lines max, not enforced) if char == "\n": self.i2c_lcd.write(bytes([self.CMD, self.NEWLINE])) continue # get the hex value of the char c = ord(char) self.i2c_lcd.write(bytes([self.CHAR, c])) # (there is a 16 char per line limit that I'm not enforcing) except I2CError as e: raise exceptions.DriverError(logger=self.logger) from e # -------------------------------------------------------------------------- def display_time(self, retry: bool = True) -> None: """Clears LCD and displays current time.""" # utc = time.gmtime() lt = time.localtime() now = "{}".format(time.strftime("%F %X", lt)) self.logger.debug("Writing time {}".format(now)) try: # command: clear display self.i2c_lcd.write(bytes([self.CMD, self.CLEAR])) time.sleep(0.05) # Wait for lcd to process self.write_string(now) except exceptions.DriverError as e: raise exceptions.DriverError(logger=self.logger) from e
class PeripheralSimulator: """I2C peripheral simulator base class.""" def __init__( self, name: str, bus: int, device_addr: int, mux_address: Optional[int], mux_channel: Optional[int], mux_simulator: Optional[MuxSimulator], ) -> None: # Initialize parameters self.name = name self.bus = bus self.device_addr = device_addr self.mux_address = mux_address self.mux_channel = mux_channel self.mux_simulator = mux_simulator # Initialize logger logname = "Simulator({})".format(name) self.logger = Logger(logname, __name__) self.logger.debug("Initializing simulator") # Initialize buffer self.buffer: bytearray = bytearray([]) # mutable bytes # Initialize register self.registers: Dict[int, int] = {} self.writes: Dict[str, bytes] = {} def __enter__(self) -> object: """Context manager enter function.""" return self def __exit__(self, exc_type: ET, exc_val: EV, exc_tb: EB) -> bool: """Context manager exit function, ensures resources are cleaned up.""" return False # Don't suppress exceptions @verify_mux def read(self, device_addr: int, num_bytes: int) -> bytes: """Reads bytes from buffer. Returns 0x00 if buffer is empty.""" msg = "Reading {} bytes, buffer: {}".format(num_bytes, byte_str(self.buffer)) self.logger.debug(msg) # Check device address matches if device_addr != self.device_addr: message = "Address not found: 0x{:02X}".format(device_addr) raise ReadError(message) # Pop bytes from buffer and return bytes_ = [] while num_bytes > 0: # Check for empty buffer or pop byte from buffer if len(self.buffer) == 0: bytes_.append(0x00) else: bytes_.append(self.buffer.pop()) # Decrement num bytes to read num_bytes = num_bytes - 1 # Successfully read bytes return bytes(bytes_) def write(self, address: int, bytes_: bytes) -> None: """Writes bytes to buffer.""" # Check if writing to mux if address == self.mux_address: # Check if mux command valid if len(bytes_) > 1: raise MuxError("Unable to set mux, only 1 command byte is allowed") # Set mux to channel self.mux_simulator.set(self.mux_address, bytes_[0]) # type: ignore # Check if writing to device elif address == self.device_addr: # Verify mux connection if self.mux_address != None: address = self.mux_address # type: ignore channel = self.mux_channel self.mux_simulator.verify(address, channel) # type: ignore # Get response bytes response_bytes = self.writes.get(byte_str(bytes_), None) # Verify known write bytes if response_bytes == None: raise WriteError("Unknown write bytes: {}".format(byte_str(bytes_))) # Write response bytes to buffer response_byte_string = byte_str(response_bytes) # type: ignore self.logger.debug("Response bytes: {}".format(response_byte_string)) for byte in response_bytes: # type: ignore self.buffer.insert(0, byte) self.logger.debug("Buffer: {}".format(byte_str(self.buffer))) # Check for invalid address else: message = "Address not found: 0x{:02X}".format(address) raise WriteError(message) @verify_mux def read_register(self, device_addr: int, register_addr: int) -> int: """Reads register byte.""" # Check address matches if device_addr != self.device_addr: message = "Address not found: 0x{:02X}".format(device_addr) raise ReadError(message) # Check register within range if register_addr not in range(256): message = "Invalid register addrress: {}, must be 0-255".format( register_addr ) raise ReadError(message) # Read register value from register dict try: return self.registers[register_addr] except KeyError: message = "Register address not found: 0x{:02X}".format(register_addr) raise ReadError(message) @verify_mux def write_register(self, device_addr: int, register_addr: int, value: int) -> None: """Writes byte to register.""" # Check address matches if device_addr != self.device_addr: message = "Device address not found: 0x{:02X}".format(device_addr) raise WriteError(message) # Check register within range if register_addr not in range(256): message = "Invalid register addrress: {}, must be 0-255".format( register_addr ) raise WriteError(message) # Check value within range if value not in range(256): message = "Invalid register value: {}, must be 0-255".format(value) raise WriteError(message) # Write value to register self.registers[register_addr] = value
class LEDDAC5578Driver: """Driver for array of led panels controlled by a dac5578.""" # Initialize var defaults num_active_panels = 0 num_expected_panels = 1 def __init__( self, name: str, panel_configs: List[Dict[str, Any]], panel_properties: Dict[str, Any], i2c_lock: threading.Lock, simulate: bool = False, mux_simulator: Optional[MuxSimulator] = None, ) -> None: """Initializes driver.""" # Initialize driver parameters self.panel_properties = panel_properties self.i2c_lock = i2c_lock self.simulate = simulate # Initialize logger self.logger = Logger(name="Driver({})".format(name), dunder_name=__name__) # Parse panel properties self.channels = self.panel_properties.get("channels") self.dac_map = self.panel_properties.get("dac_map") # Initialze num expected panels self.num_expected_panels = len(panel_configs) # Initialize panels self.panels: List[LEDDAC5578Panel] = [] for config in panel_configs: panel = LEDDAC5578Panel(name, config, i2c_lock, simulate, mux_simulator, self.logger) panel.initialize() self.panels.append(panel) # Check at least one panel is still active active_panels = [ panel for panel in self.panels if not panel.is_shutdown ] self.num_active_panels = len(active_panels) if self.num_active_panels < 1: raise NoActivePanelsError(logger=self.logger) # Successfully initialized message = "Successfully initialized with {} ".format( self.num_active_panels) message2 = "active panels, expected {}".format( self.num_expected_panels) self.logger.debug(message + message2) def turn_on(self) -> Dict[str, float]: """Turns on leds.""" self.logger.debug("Turning on") channel_outputs = self.build_channel_outputs(100) self.set_outputs(channel_outputs) return channel_outputs def turn_off(self) -> Dict[str, float]: """Turns off leds.""" self.logger.debug("Turning off") channel_outputs = self.build_channel_outputs(0) self.set_outputs(channel_outputs) return channel_outputs def set_spd( self, desired_distance: float, desired_intensity: float, desired_spectrum: Dict ) -> Tuple[Optional[Dict], Optional[Dict], Optional[Dict]]: """Sets spectral power distribution.""" message = "Setting spd, distance={}cm, ppfd={}umol/m2/s, spectrum={}".format( desired_distance, desired_intensity, desired_spectrum) self.logger.debug(message) # Approximate spectral power distribution try: channel_outputs, output_spectrum, output_intensity = light.approximate_spd( self.panel_properties, desired_distance, desired_intensity, desired_spectrum, ) except Exception as e: message = "approximate spd failed" raise SetSPDError(message=message, logger=self.logger) from e # Set outputs self.set_outputs(channel_outputs) # Successfully set channel outputs message = "Successfully set spd, output: channels={}, spectrum={}, intensity={}umol/m2/s".format( channel_outputs, output_spectrum, output_intensity) self.logger.debug(message) return (channel_outputs, output_spectrum, output_intensity) def set_outputs(self, par_setpoints: dict) -> None: """Sets outputs on light panels. Converts channel names to channel numbers, translates par setpoints to dac setpoints, then sets dac.""" self.logger.debug("Setting outputs: {}".format(par_setpoints)) # Check at least one panel is active active_panels = [ panel for panel in self.panels if not panel.is_shutdown ] self.num_active_panels = len(active_panels) if self.num_active_panels < 1: raise NoActivePanelsError(logger=self.logger) message = "Setting outputs on {} active panels".format( self.num_active_panels) self.logger.debug(message) # Convert channel names to channel numbers converted_outputs = {} for name, percent in par_setpoints.items(): # Convert channel name to channel number try: number = self.get_channel_number(name) except Exception as e: raise SetOutputsError(logger=self.logger) from e # Append to converted outputs converted_outputs[number] = percent # Try to set outputs on all panels for panel in self.panels: # Scale setpoints dac_setpoints = self.translate_setpoints(converted_outputs) # Set outputs on panel try: panel.driver.write_outputs(dac_setpoints) # type: ignore except AttributeError: message = "Unable to set outputs on `{}`".format(panel.name) self.logger.error(message + ", panel not initialized") except Exception as e: message = "Unable to set outputs on `{}`".format(panel.name) self.logger.exception(message) panel.is_shutdown = True # TODO: Check for new events in manager # Manager event functions can get called any time # As a special case, use function to set a new_event flag in driver # Check it here and break if panel failed # Only on panel failures b/c only ultra case leading to high latency.. # at 5 seconds per failed panel (due to retry) # In a grid like SMHC, 25 panels * 5 seconds is grueling... # How to clear flag?...event handler process funcs all remove it # Check at least one panel is still active active_panels = [ panel for panel in self.panels if not panel.is_shutdown ] self.num_active_panels = len(active_panels) if self.num_active_panels < 1: message = "failed when setting outputs" raise NoActivePanelsError(message=message, logger=self.logger) def set_output(self, channel_name: str, par_setpoint: float) -> None: """Sets output on light panels. Converts channel name to channel number, translates par setpoint to dac setpoint, then sets dac.""" self.logger.debug("Setting ch {}: {}".format(channel_name, par_setpoint)) # Check at least one panel is active active_panels = [ panel for panel in self.panels if not panel.is_shutdown ] if len(active_panels) < 1: raise NoActivePanelsError(logger=self.logger) message = "Setting output on {} active panels".format( self.num_active_panels) self.logger.debug(message) # Convert channel name to channel number try: channel_number = self.get_channel_number(channel_name) except Exception as e: raise SetOutputError(logger=self.logger) from e # Set output on all panels for panel in self.panels: # Scale setpoint dac_setpoint = self.translate_setpoint(par_setpoint) # Set output on panel try: panel.driver.write_output(channel_number, dac_setpoint) # type: ignore except AttributeError: message = "Unable to set output on `{}`".format(panel.name) self.logger.error(message + ", panel not initialized") except Exception as e: message = "Unable to set output on `{}`".format(panel.name) self.logger.exception(message) panel.is_shutdown = True # Check at least one panel is still active active_panels = [ panel for panel in self.panels if not panel.is_shutdown ] self.num_active_panels = len(active_panels) if self.num_active_panels < 1: message = "failed when setting output" raise NoActivePanelsError(message=message, logger=self.logger) def get_channel_number(self, channel_name: str) -> int: """Gets channel number from channel name.""" try: channel_dict = self.channels[channel_name] # type: ignore channel_number = channel_dict.get("port", -1) return int(channel_number) except KeyError: raise InvalidChannelNameError(message=channel_name, logger=self.logger) def get_channels(self) -> Dict: return self.channels def build_channel_outputs(self, value: float) -> Dict[str, float]: """Build channel outputs. Sets each channel to provided value.""" self.logger.debug("Building channel outputs") channel_outputs = {} for key in self.channels.keys(): # type: ignore channel_outputs[key] = value self.logger.debug("channel outputs = {}".format(channel_outputs)) return channel_outputs def translate_setpoints(self, par_setpoints: Dict) -> Dict: """Translates par setpoints to dac setpoints.""" self.logger.debug("Translating setpoints") # Build interpolation lists dac_list = [] par_list = [] for dac_percent, par_percent in self.dac_map.items(): # type: ignore dac_list.append(float(dac_percent)) par_list.append(float(par_percent)) self.logger.debug("dac_list = {}".format(dac_list)) self.logger.debug("par_list = {}".format(par_list)) # Get dac setpoints dac_setpoints = {} for key, par_setpoint in par_setpoints.items(): dac_setpoint = maths.interpolate(par_list, dac_list, par_setpoint) dac_setpoints[key] = dac_setpoint # Successfully translated dac setpoints self.logger.debug("Translated setpoints from {} to {}".format( par_setpoints, dac_setpoints)) return dac_setpoints def translate_setpoint(self, par_setpoint: float) -> float: """Translates par setpoint to dac setpoint.""" # Build interpolation lists dac_list = [] par_list = [] for dac_percent, par_percent in self.dac_map.items(): # type: ignore dac_list.append(float(dac_percent)) par_list.append(float(par_percent)) # Get dac setpint dac_setpoint = maths.interpolate(par_list, dac_list, par_setpoint) # Successfully translated dac setpoint return dac_setpoint # type: ignore