Example #1
0
class PhenoboxStateMachine(Machine):
    camera_controller = None
    code_scanner = None
    motor_controller = None
    led_controller = None
    image_handler = None
    illumination = None
    code_information = None
    plant = None
    pic_count = 0
    error_code = 0

    error_messages = {
        1: "Unable to read QR-Code",
        2: "Unable to upload pictures",
        3: "Door was opened unexpectedly",
        4: "Unable to capture picture",
        5: "Unable to connect to camera",
        6: "Unable to retrieve data",
        7: "Unable to authenticate",
        8: "Unable to get picture from camera",
        9: "Unable to create snapshot on server",
        10: "This plant has already been processed for the current timestamp",
        11: "Unable to connect to server",
        12: "QR-Code has unknown meaning"
    }

    def __init__(self,
                 camera_controller,
                 code_scanner,
                 motor_controller,
                 led_controller,
                 image_handler,
                 illumination,
                 photo_count=6):

        self.camera_controller = camera_controller
        self.code_scanner = code_scanner
        self.motor_controller = motor_controller
        self.led_controller = led_controller
        self.image_handler = image_handler
        self.illumination = illumination
        self.photo_count = photo_count
        # this is used to split QR code data into several fields:
        self.qrcode_pattern = re.compile('^([^ ]+) ([^ ]+) ([^ ]+) ([^ ]+).*$')

        self._logger = logging.getLogger(__name__)
        self._logger.setLevel(logging.INFO)

        states = [
            'STARTUP',
            State(name='IDLE', on_enter='on_enter_idle'),
            State(name='AUTH', on_enter='on_enter_auth'),
            State(name='TAKE_FIRST_PICTURE',
                  on_enter='on_enter_take_first_picture'),
            State(name='ANALYZE_PICTURE', on_enter='on_enter_analyze_picture'),
            State(name='SETUP', on_enter='on_enter_setup'),
            State(name='DRIVE', on_enter='on_enter_drive'),
            State(name='TAKE_PICTURE', on_enter='on_enter_take_picture'),
            State(name='RETURN', on_enter='on_enter_return'),
            State(name='UPLOAD', on_enter='on_enter_upload'),
            State(name='ERROR',
                  on_enter='on_enter_error',
                  on_exit='on_exit_error',
                  ignore_invalid_triggers=True)
        ]

        Machine.__init__(self,
                         states=states,
                         send_event=True,
                         initial="STARTUP")
        self.add_transition('initialize',
                            'STARTUP',
                            'RETURN',
                            after=[self.after_return])

        # ----FROM IDLE----#
        self.add_transition('start',
                            'IDLE',
                            'SETUP',
                            after=[self.after_start],
                            conditions=[self.valid_authentication])
        self.add_transition('start',
                            'IDLE',
                            'AUTH',
                            unless=[self.valid_authentication])
        # ----FROM AUTH----#
        self.add_transition('start', 'AUTH', 'SETUP', after=[self.after_start])
        self.add_transition('error', 'AUTH', 'ERROR')
        # ----FROM TAKE_FIRST_PICTURE----#
        self.add_transition('analyze',
                            'TAKE_FIRST_PICTURE',
                            'ANALYZE_PICTURE',
                            after=[self.after_analyze])
        self.add_transition('error', 'TAKE_FIRST_PICTURE', 'ERROR')
        # ----FROM ANALYZE_PICTURE----#
        self.add_transition('rotate',
                            'ANALYZE_PICTURE',
                            'DRIVE',
                            after=[self.after_rotate])
        self.add_transition('error', 'ANALYZE_PICTURE', 'ERROR')
        # ----FROM SETUP----#
        self.add_transition('take_first',
                            'SETUP',
                            'TAKE_FIRST_PICTURE',
                            after=[self.after_take_first])
        # ----FROM DRIVE----#
        self.add_transition('picture',
                            'DRIVE',
                            'TAKE_PICTURE',
                            after=[self.after_picture])
        # ----FROM TAKE_PICTURE----#
        self.add_transition('error', "TAKE_PICTURE", 'ERROR')
        self.add_transition('next_picture',
                            'TAKE_PICTURE',
                            'DRIVE',
                            after=[self.after_rotate],
                            unless=[self.enough_pictures])
        self.add_transition('next_picture',
                            'TAKE_PICTURE',
                            'UPLOAD',
                            after=[self.after_upload],
                            conditions=[self.enough_pictures])
        # ----FROM RETURN----#
        self.add_transition('idle', 'RETURN', 'IDLE')
        # ----FROM UPLOAD----#
        self.add_transition('upload_finished',
                            'UPLOAD',
                            'RETURN',
                            after=[self.after_return])
        # ----FROM ERROR----#
        self.add_transition('restart',
                            'ERROR',
                            'SETUP',
                            after=[self.after_start],
                            conditions=[self.valid_authentication])
        self.add_transition('restart',
                            'ERROR',
                            'AUTH',
                            unless=[self.valid_authentication])
        # ----DOOR----#
        self.add_transition('door_opened', [
            'TAKE_FIRST_PICTURE', 'ANALYZE_PICTURE', 'SETUP', 'DRIVE',
            'TAKE_PICTURE', 'UPLOAD', 'RETURN', 'AUTH'
        ], 'ERROR')

    def valid_authentication(self, event):
        return True

    def on_enter_return(self, event):
        print(Fore.BLUE + 'Returning to Origin')
        # time.sleep(1)
        self.motor_controller.return_to_origin()

    def after_return(self, event):
        self.idle()

    def on_enter_idle(self, event):
        print(Fore.GREEN + 'Idle. Waiting for user Input')
        self.led_controller.clear_all()
        self.led_controller.switch_green(True)
        self.code_information = None
        self.plant = None

    def on_enter_auth(self, event):
        print(Fore.BLUE + 'Skipping Authentication')
        self.led_controller.switch_green(False)
        self.led_controller.switch_blue(True)
        self.start()

    def on_enter_setup(self, event):
        print(Fore.BLUE + 'Setting up')
        self.led_controller.switch_green(False)
        self.led_controller.switch_blue(True)
        self.motor_controller.return_to_origin()
        self.plant = Plant()

    def after_start(self, event):
        self.take_first()

    def on_enter_take_first_picture(self, event):
        pict_name = str(uuid.uuid4())
        print(Fore.BLUE + 'Taking first picture: {}'.format(pict_name))
        try:
            self.illumination.set_illumination(100)
            time.sleep(1)
            picture_path = self.camera_controller.capture_and_download(
                pict_name)
            print('Picture path: "{}"'.format(picture_path))
            if picture_path is None:
                self.error(error_code=8)
            else:
                self.plant.add_picture(picture_path,
                                       self.motor_controller.current_angle)
                self.analyze()

        except ConnectionError as err:
            self._logger.error(err.message)
            self.error(error_code=5)
        except CaptureError as err:
            self._logger.error(err.message)
            self.error(error_code=4)
        finally:
            self.illumination.set_illumination(1)

    def after_take_first(self, event):
        pass
        # self.analyze()

    def on_enter_analyze_picture(self, event):
        print(Fore.BLUE + 'Analyzing')
        path, _ = self.plant.get_first_picture()
        self.code_information = self.code_scanner.scan_image(path)
        self.plant.name = self.code_information.decode('utf8')

    def after_analyze(self, event):
        if self.code_information is not None:
            qrcode = self.code_information.decode('utf8')
            print(Fore.CYAN + 'Decoded symbol "{}"'.format(qrcode))
            self._logger.info('decoded symbol "{}"'.format(qrcode))
            m = self.qrcode_pattern.match(qrcode)
            if not m:
                self.error(error_code=12)
                return

            plant_id = m.group(1)

            # the following in the original phenobox comes from a query to the server,
            # here we create the property values from the scanned QR code instead.
            self.plant.experiment_name = m.group(1)  # experiment (project)
            self.plant.timestamp_id = m.group(2)  # time point
            self.plant.snapshot_id = m.group(3)  # treatment
            self.plant.sample_group_name = m.group(4)  # replicate
        else:
            self.error(error_code=1)
            return
        self.rotate()

    def on_enter_drive(self, event):
        print(Fore.BLUE + 'Driving')
        # time.sleep(1)
        self.motor_controller.move_to_position(self.plant.get_picture_count())

    def after_rotate(self, event):
        time.sleep(1)
        self.picture()

    def on_enter_take_picture(self, event):
        print(Fore.BLUE + 'Taking picture at angle ' +
              str(self.motor_controller.current_angle))
        try:
            picture_path = self.camera_controller.capture_and_download(
                str(uuid.uuid4()))
            if picture_path is None:
                self.error(error_code=6)
            else:
                self.plant.add_picture(picture_path,
                                       self.motor_controller.current_angle)
                #copy(picture_path, '/home/pi/Phenobox/smbmnt')
                self.next_picture()
        except CaptureError:
            self.error(error_code=4)

    def enough_pictures(self, event):
        return self.plant.get_picture_count() >= self.photo_count

    def after_picture(self, event):
        pass

    def on_enter_upload(self, event):
        print(Fore.BLUE + 'Dispatching picture tasks')
        self.led_controller.blink_orange()
        self.image_handler.add_plant(self.plant)

    # TODO rename to after_upload_dispatched
    def after_upload(self, event):
        self.led_controller.switch_orange(False)
        self.upload_finished()

    def on_enter_error(self, event):
        print(Fore.RED + 'Something wrong happened')
        error_code = event.kwargs.get('error_code', -1)
        # Reset state variables
        self.pic_count = 0
        self.code_information = None
        if self.plant is not None:
            self.plant.delete_all_pictures()
        self.plant = None

        msg = self.error_messages.get(error_code, "Unknown error")
        self._logger.info(
            'Machine transitioned to error state with code {}. ({})'.format(
                error_code, msg))
        print(Fore.RED + msg)
        self.led_controller.clear_all()
        if error_code == 1:
            self.led_controller.switch_orange(True)
        elif error_code == 2 or error_code == 6 or error_code == 7 or error_code == 9 or error_code == 11:
            self.led_controller.switch_red(True)
        elif error_code == 3:
            self.led_controller.blink_orange()
            self.led_controller.blink_blue()
        elif error_code == 4 or error_code == 5 or error_code == 8:
            self.led_controller.blink_orange()
        elif error_code == 10:
            self.led_controller.blink_blue()

    def on_exit_error(self, event):
        self.led_controller.clear_all()
Example #2
0
class PhenoboxMachine(Machine):
    camera_controller = None
    code_scanner = None
    motor_controller = None
    led_controller = None
    image_handler = None
    auth = None
    code_information = None
    plant = None
    pic_count = 0
    error_code = 0

    error_messages = {
        1: "Unable to read QR-Code",
        2: "Unable to upload pictures",
        3: "Door was opened unexpectedly",
        4: "Unable to capture picture",
        5: "Unable to connect to camera",
        6: "Unable to retrieve data",
        7: "Unable to authenticate",
        8: "Unable to get picture from camera",
        9: "Unable to create snapshot on server",
        10: "This plant has already been processed for the current timestamp",
        11: "Unable to connect to server"
    }

    def __init__(self,
                 camera_controller,
                 code_scanner,
                 motor_controller,
                 led_controller,
                 image_handler,
                 auth,
                 photo_count=6):

        self._logger = logging.getLogger(__name__)
        self._logger.setLevel(logging.INFO)

        states = [
            'STARTUP',
            State(name='IDLE', on_enter='on_enter_idle'),
            State(name='AUTH', on_enter='on_enter_auth'),
            State(name='TAKE_FIRST_PICTURE',
                  on_enter='on_enter_take_first_picture'),
            State(name='ANALYZE_PICTURE', on_enter='on_enter_analyze_picture'),
            State(name='SETUP', on_enter='on_enter_setup'),
            State(name='DRIVE', on_enter='on_enter_drive'),
            State(name='TAKE_PICTURE', on_enter='on_enter_take_picture'),
            State(name='RETURN', on_enter='on_enter_return'),
            State(name='UPLOAD', on_enter='on_enter_upload'),
            State(name='ERROR',
                  on_enter='on_enter_error',
                  on_exit='on_exit_error',
                  ignore_invalid_triggers=True)
        ]

        Machine.__init__(self,
                         states=states,
                         send_event=True,
                         initial="STARTUP")
        self.add_transition('initialize',
                            'STARTUP',
                            'RETURN',
                            after=[self.after_return])

        # ----FROM IDLE----#
        self.add_transition('start',
                            'IDLE',
                            'SETUP',
                            after=[self.after_start],
                            conditions=[self.valid_authentication])
        self.add_transition('start',
                            'IDLE',
                            'AUTH',
                            unless=[self.valid_authentication])
        # ----FROM AUTH----#
        self.add_transition('start', 'AUTH', 'SETUP', after=[self.after_start])
        self.add_transition('error', 'AUTH', 'ERROR')
        # ----FROM TAKE_FIRST_PICTURE----#
        self.add_transition('analyze',
                            'TAKE_FIRST_PICTURE',
                            'ANALYZE_PICTURE',
                            after=[self.after_analyze])
        self.add_transition('error', 'TAKE_FIRST_PICTURE', 'ERROR')
        # ----FROM ANALYZE_PICTURE----#
        self.add_transition('rotate',
                            'ANALYZE_PICTURE',
                            'DRIVE',
                            after=[self.after_rotate])
        self.add_transition('error', 'ANALYZE_PICTURE', 'ERROR')
        # ----FROM SETUP----#
        self.add_transition('take_first',
                            'SETUP',
                            'TAKE_FIRST_PICTURE',
                            after=[self.after_take_first])
        # ----FROM DRIVE----#
        self.add_transition('picture',
                            'DRIVE',
                            'TAKE_PICTURE',
                            after=[self.after_picture])
        # ----FROM TAKE_PICTURE----#
        self.add_transition('error', "TAKE_PICTURE", 'ERROR')
        self.add_transition('next_picture',
                            'TAKE_PICTURE',
                            'DRIVE',
                            after=[self.after_rotate],
                            unless=[self.enough_pictures])
        self.add_transition('next_picture',
                            'TAKE_PICTURE',
                            'UPLOAD',
                            after=[self.after_upload],
                            conditions=[self.enough_pictures])
        # ----FROM RETURN----#
        self.add_transition('idle', 'RETURN', 'IDLE')
        # ----FROM UPLOAD----#
        self.add_transition('upload_finished',
                            'UPLOAD',
                            'RETURN',
                            after=[self.after_return])
        # ----FROM ERROR----#
        self.add_transition('restart',
                            'ERROR',
                            'SETUP',
                            after=[self.after_start],
                            conditions=[self.valid_authentication])
        self.add_transition('restart',
                            'ERROR',
                            'AUTH',
                            unless=[self.valid_authentication])
        # ----DOOR----#
        self.add_transition('door_opened', [
            'TAKE_FIRST_PICTURE', 'ANALYZE_PICTURE', 'SETUP', 'DRIVE',
            'TAKE_PICTURE', 'UPLOAD', 'RETURN', 'AUTH'
        ], 'ERROR')

        self.camera_controller = camera_controller
        self.code_scanner = code_scanner
        self.motor_controller = motor_controller
        self.led_controller = led_controller
        self.image_handler = image_handler
        self.photo_count = photo_count

        self.auth = auth

    def valid_authentication(self, event):
        return self.auth.is_authenticated() or self.auth.can_refresh()

    def on_enter_return(self, event):
        print(Fore.BLUE + 'Returning to Origin')
        # time.sleep(1)
        self.motor_controller.return_to_origin()

    def after_return(self, event):
        self.idle()

    def on_enter_idle(self, event):
        print(Fore.GREEN + 'Idle. Waiting for user Input')
        self.led_controller.clear_all()
        self.led_controller.switch_green(True)
        self.code_information = None
        self.plant = None

    def on_enter_auth(self, event):
        print(Fore.BLUE + 'Authenticating')
        self.led_controller.switch_green(False)
        self.led_controller.switch_blue(True)
        try:
            if self.auth.check_and_auth():
                self.start()
            else:
                self.error(error_code=7)
        except ServerConnectionError as e:
            self._logger.error(e.message)
            self.error(error_code=11)

    def on_enter_setup(self, event):
        print(Fore.BLUE + 'Setting up')
        self.led_controller.switch_green(False)
        self.led_controller.switch_blue(True)
        self.motor_controller.return_to_origin()
        self.plant = Plant()

    def after_start(self, event):
        self.take_first()

    def on_enter_take_first_picture(self, event):
        print(Fore.BLUE + 'Taking first picture')
        try:
            picture_path = self.camera_controller.capture_and_download(
                name=str(uuid.uuid4()))
            if picture_path is None:
                self.error(error_code=8)
            else:
                self.plant.add_picture(picture_path,
                                       self.motor_controller.current_angle)
                self.analyze()
        except ConnectionError as err:
            self._logger.error(err.message)
            self.error(error_code=5)
        except CaptureError as err:
            self._logger.error(err.message)
            self.error(error_code=4)

    def after_take_first(self, event):
        pass
        # self.analyze()

    def on_enter_analyze_picture(self, event):
        print(Fore.BLUE + 'Analyzing')
        path, _ = self.plant.get_first_picture()
        self.code_information = self.code_scanner.scan_image(path)

    def after_analyze(self, event):
        if self.code_information is not None:
            print(Fore.CYAN +
                  'Decoded symbol "%s"' % str(self.code_information))
            self._logger.info('decoded symbol {}'.format(
                self.code_information))
            plant_id = self.code_information
            try:
                s = Session()
                graphql_address = '{}{}'.format(
                    getattr(config, 'cfg').get('server', 'base_address'),
                    getattr(config, 'cfg').get('server', 'graphql_endpoint'))
                prepped_req = self.auth.prep_post(
                    graphql_address,
                    data={
                        'query':
                        '{plant(id:"' + plant_id +
                        '"){id index name fullName sampleGroup {name experiment {name}}}}'
                    })
                r = s.send(prepped_req)
                plant = r.json()['data']['plant']
                if plant is None:
                    self.error(error_code=6)
                    return
                else:
                    # Create Snapshot
                    param_string = 'plantId:"{}", cameraPosition:"{}", measurementTool:"{}", phenoboxId:"{}"' \
                        .format(plant['id'],
                                getattr(config, 'cfg').get('box', 'camera_position'),
                                getattr(config, 'cfg').get('box', 'measurement_tool'),
                                getattr(config, 'cfg').get('box', 'id'))
                    prepped_req = self.auth.prep_post(
                        graphql_address,
                        data={
                            'query':
                            'mutation {createSnapshot(' + param_string +
                            '){id timestampId}}'
                        })
                    r = s.send(prepped_req)
                    if r.status_code != 200:
                        # TODO get exception
                        self._logger.info(
                            'Unable to create Snapshot. HTML status code {}. Param String : {}'
                            .format(r.status_code, param_string))
                        self.error(error_code=9)
                        return
                        # do something
                    resp = r.json()
                    if resp.get('errors', None):
                        if resp.get('errors')[0]['code'] == 409:
                            self._logger.info('Snapshot already exists')
                            self.error(error_code=10)
                        else:
                            self._logger.error(
                                'Error while trying to create snapshot. Param String : {}. Error: {}'
                                .format(param_string,
                                        resp.get('errors')[0]['message']))
                            self.error(error_code=9)

                        return
                    snapshot_id = resp['data']['createSnapshot']['id']
                    timestamp_id = resp['data']['createSnapshot'][
                        'timestampId']

                name = plant['fullName']
                index = plant['index']
                sample_group = plant['sampleGroup']['name']
                experiment = plant['sampleGroup']['experiment']['name']
                # TODO add seperate step/state to retrieve data and set plant properties
                self.plant.experiment_name = experiment
                self.plant.sample_group_name = sample_group
                self.plant.name = name
                self.plant.index = index
                self.plant.snapshot_id = snapshot_id
                self.plant.timestamp_id = timestamp_id

            except UnableToAuthenticateError:
                self.error(error_code=7)
            except ServerConnectionError as e:
                self._logger.error(e.message)
                self.error(error_code=11)
        else:
            self.error(error_code=1)
            return
        self.rotate()

    def on_enter_drive(self, event):
        print(Fore.BLUE + 'Driving')
        # time.sleep(1)
        self.motor_controller.move_to_position(self.plant.get_picture_count())

    def after_rotate(self, event):
        time.sleep(1)
        self.picture()

    def on_enter_take_picture(self, event):
        print(Fore.BLUE + 'Taking picture at angle ' +
              str(self.motor_controller.current_angle))
        try:
            picture_path = self.camera_controller.capture_and_download(
                str(uuid.uuid4()))
            if picture_path is None:
                self.error(error_code=6)
            else:
                self.plant.add_picture(picture_path,
                                       self.motor_controller.current_angle)
                self.next_picture()
        except CaptureError:
            self.error(error_code=4)

    def enough_pictures(self, event):
        return self.plant.get_picture_count() >= self.photo_count

    def after_picture(self, event):
        pass

    def on_enter_upload(self, event):
        print(Fore.BLUE + 'Dispatching picture tasks')
        self.image_handler.add_plant(self.plant)

    # TODO rename to after_upload_dispatched
    def after_upload(self, event):
        self.upload_finished()

    def on_enter_error(self, event):
        print(Fore.RED + 'Something wrong happened')
        error_code = event.kwargs.get('error_code', -1)
        # Reset state variables
        self.pic_count = 0
        self.code_information = None
        if self.plant is not None:
            self.plant.delete_all_pictures()
        self.plant = None

        msg = self.error_messages.get(error_code, "Unknown error")
        self._logger.info(
            'Machine transitioned to error state with code {}. ({})'.format(
                error_code, msg))
        print(Fore.RED + msg)
        self.led_controller.clear_all()
        if error_code == 1:
            self.led_controller.switch_orange(True)
        elif error_code == 2 or error_code == 6 or error_code == 7 or error_code == 9 or error_code == 11:
            self.led_controller.switch_red(True)
        elif error_code == 3:
            self.led_controller.blink_orange()
            self.led_controller.blink_blue()
        elif error_code == 4 or error_code == 5 or error_code == 8:
            self.led_controller.blink_orange()
        elif error_code == 10:
            self.led_controller.blink_blue()

    def on_exit_error(self, event):
        self.led_controller.clear_all()