示例#1
0
class DgSyncThread(Thread):
    viewer_name_wildcard = '.* \[Camer.*\]$'  # Looking for window with name Scene_Name * [Camera]
    dg_window_name_wildcard = 'DELTAGEN .*'  # Look for DeltaGen Window name

    def __init__(self, viewer):
        """ Worker object to sync DG Viewer to Image Viewer position and size

        :param modules.img_view.ImageView viewer: Image viewer parent
        """
        super(DgSyncThread, self).__init__()
        self.viewer = viewer
        self.win_mgr = Win32WindowMgr()

        # --- External event to end thread ---
        self.exit_event = Event()

        self.dg_btn_timeout = QTimer()
        self.dg_btn_timeout.setInterval(4000)
        self.dg_btn_timeout.setSingleShot(True)
        self.dg_btn_timeout.timeout.connect(self.dg_reset_btn)

        self.sync_dg = False
        self.pull_viewer_foreground = False
        self.initial_sync_started = True
        self.initial_viewer_size = str()
        self.last_known_win32_wrapper = None

        self.ncat = Ncat(DG_TCP_IP,
                         KnechtSettings.app.get('port', DG_TCP_PORT))

        self.signals = DgSyncThreadSignals()
        self.set_btn_enabled_signal = self.signals.set_btn_enabled_signal
        self.set_btn_enabled_signal.connect(self.viewer.dg_toggle_btn)
        self.set_btn_checked_signal = self.signals.set_btn_checked_signal
        self.set_btn_checked_signal.connect(self.viewer.dg_check_btn)
        self.message = self.signals.message_signal
        self.progress = self.signals.progress_signal

        self.signals.position_img_viewer_signal.connect(self.viewer.move)

    def run(self):
        """ Thread loop running until exit_event set. As soon as a new send operation
            is scheduled, loop will pick up send operation on next loop cycle.
        """
        while not self.exit_event.is_set():
            sync_refresh_rate = 1.5  # seconds
            if self.sync_dg:
                if not self.sync_img_viewer():
                    # Not synced, toggle off
                    LOGGER.debug('Sync unsuccessful, stopping sync.')
                    self.dg_toggle_sync(False)
                    self.message.emit(
                        _('Synchronisation beendet. Keine Verbindung zum DeltaGen Host oder kein Viewer'
                          'Fenster gefunden.'))
                else:
                    # Synced
                    sync_refresh_rate = 0.8  # sync quicker if enabled
                    self.pull_dg_focus()
            self.exit_event.wait(timeout=sync_refresh_rate)

        self.dg_close_connection()

    def dg_reset_btn(self):
        self.set_btn_enabled_signal.emit(True)

    def sync_img_viewer(self) -> bool:
        """ Resize DG Viewer widget and move img viewer to DG Viewer widget position """
        if self.initial_sync_started:
            self.progress.emit(10)
            if not self.ncat.deltagen_is_alive():
                LOGGER.info(
                    'No socket connected to DeltaGen or no Viewer window active/in focus.'
                )
                return False

        # -- Sync window position
        sync_result = self.sync_window_position()

        if not sync_result:
            return False

        # -- Sync DeltaGen Viewer size
        size = f'{self.viewer.size().width()} {self.viewer.size().height()}'
        command = f'UNFREEZE VIEWER;SIZE VIEWER {size};'
        try:
            self.ncat.send(command)
            self.ncat.receive(timeout=0.1, log_empty=False)
        except Exception as e:
            LOGGER.error('Sending viewer size command failed. %s', e)
            return False

        return True

    def _find_dg_viewer(self):
        MeasureExecTime.start()
        dg_viewer = self.win_mgr.find_deltagen_viewer_widget()
        MeasureExecTime.finish('Find DG Viewer widget took')

        if not dg_viewer:
            LOGGER.info('Could not find DeltaGen Viewer widget.')
            return
        return dg_viewer

    def sync_window_position(self) -> bool:
        """ Position the image viewer over the DeltaGen Viewport viewer """
        if not self.win_mgr.has_handle():
            return False

        if self.last_known_win32_wrapper is None or self.initial_sync_started:
            # Find pywinauto window handle
            MeasureExecTime.start()
            dg_viewer = self._find_dg_viewer()
            if not dg_viewer:
                return False

            self.last_known_win32_wrapper = dg_viewer.wrapper_object()
            MeasureExecTime.finish('Finding DG Viewer widget took')

        # - Get viewer OpenGl Area rectangle
        try:
            r = self.last_known_win32_wrapper.rectangle()
        except Exception as e:
            LOGGER.debug('Last known window handle invalid. %s', e)
            self.last_known_win32_wrapper = None
            return False

        # - Convert to QRect and QPoint
        x, y, w, h = r.left, r.top, r.width(), r.height()

        if self.initial_sync_started:
            # Save initial viewer size before syncing
            self.initial_viewer_size = f'{w} {h}'

        if x + y + w + h < 1:
            # Empty values indicate a destroyed window
            return False

        viewer_rect = QRect(x, y, w, h)

        # - Check if is inside screen limits
        # (minimizing the window will move it's position to eg. -33330)
        if self.viewer.is_inside_limit(self.viewer.calculate_screen_limits(),
                                       viewer_rect.topLeft()):
            LOGGER.debug('DeltaGen Viewer found at %s %s %s %s', x, y, w, h)
            self.signals.position_img_viewer_signal.emit(viewer_rect.topLeft())

        return True

    def dg_reset_viewer(self):
        try:
            self.ncat.send(
                f'BORDERLESS VIEWER FALSE;SIZE VIEWER {self.initial_viewer_size}'
            )
        except Exception as e:
            LOGGER.error('Sending viewer size command failed. %s', e)

        self.dg_reset_btn()

    def dg_close_connection(self):
        if self.sync_dg:
            self.dg_reset_viewer()
            self.win_mgr.clear_handle()
            self.ncat.close()

    def dg_set_camera(self, cam_info: ImageCameraInfo):
        try:
            self.ncat.send(cam_info.create_deltagen_camera_cmd())
        except Exception as e:
            LOGGER.warning('Could not trasmit camera data to DeltaGen: %s', e)

    @Slot()
    def dg_toggle_sync(self, reset_viewer: bool):
        self.sync_dg = not self.sync_dg
        self.set_btn_enabled_signal.emit(False)
        self.set_btn_checked_signal.emit(self.sync_dg)
        self.dg_btn_timeout.start()

        LOGGER.debug(f'Toggled sync {"on" if self.sync_dg else "off"}.', )

        if self.sync_dg:
            self.find_dg_window()
            self.initial_sync_started = True
            self.message.emit(
                _('Synchronisierung startet. Suche Anwendungsfenster...'))
        else:
            self.progress.emit(0)
            if reset_viewer:
                self.dg_reset_viewer()

    def find_dg_window(self):
        """ Tries to find the MS Windows window handle and pulls the viewer window to foreground """
        LOGGER.debug('Finding viewer')
        try:
            self.win_mgr.find_window_wildcard(self.dg_window_name_wildcard)
            self.initial_sync_started = True
        except Exception as e:
            LOGGER.error('Error finding DeltaGen Viewer window.\n%s', e)

    @Slot(bool)
    def viewer_toggle_pull(self, enabled: bool):
        LOGGER.debug('Setting pull_viewer_foreground: %s', not enabled)
        self.pull_viewer_foreground = not enabled

    def pull_dg_focus(self):
        # Pull DeltaGen Viewer to foreground
        if not self.pull_viewer_foreground and not self.initial_sync_started:
            return

        if not self.win_mgr.has_handle():
            return

        try:
            self.win_mgr.set_foreground()
            LOGGER.debug('Pulling DeltaGen Viewer window to foreground.')
        except Exception as e:
            LOGGER.error(
                'Error setting DeltaGen Viewer window to foreground:\n%s', e)

        # Initial pull done, do not pull to front on further sync
        self.initial_sync_started = False
示例#2
0
class CommunicateDeltaGen(Thread):
    send_operation_in_progress = False

    # Connection will be terminated if found True
    abort_connection = False

    # Variant List to send
    variants_ls = KnechtVariantList()

    # Command Queue
    command_ls = list()

    # Default Options
    freeze_viewer: bool = True
    check_variants: bool = True
    send_camera_data: bool = True
    long_render_timeout: bool = True
    display_check: bool = False
    viewer_size: str = '1280 720'
    rendering_mode = False

    _regular_receive_timeout = 0.3
    _long_receive_timeout = 1.0

    # --- Signals ---
    signals = CommunicateDeltaGenSignals()

    send_finished = signals.send_finished
    no_connection = signals.no_connection
    status = signals.status
    progress = signals.progress
    variant_status = signals.variant_status

    def __init__(self, ui):
        super(CommunicateDeltaGen, self).__init__()
        self.app = ui.app

        # --- External event to end thread ---
        self.exit_event = Event()

        # --- Socket Communication Class ---
        self.nc = Ncat(DG_TCP_IP, KnechtSettings.dg.get('port', DG_TCP_PORT))

    def run(self):
        """ Thread loop running until exit_event set. As soon as a new send operation
            is scheduled, loop will pick up send operation on next loop cycle.
        """
        while not self.exit_event.is_set():
            if self.send_operation_in_progress:
                LOGGER.debug('CommunicateDeltaGen Thread starts Variants Send operation.')
                self._send_operation()
                LOGGER.debug('CommunicateDeltaGen Thread finished Variants Send operation.')

            if self.command_ls:
                # Work down transmitted commands if no send operation active
                self._send_command_operation(self.command_ls.pop(0))

            self.exit_event.wait(timeout=0.8)

        LOGGER.debug('CommunicateDeltaGen Thread returned from run loop.')

    @Slot(bool)
    def set_rendering_mode(self, val: bool):
        """ En-/Disable rendering mode with increased connection timeouts """
        self.rendering_mode = val

    @Slot(KnechtVariantList)
    def set_variants_ls(self, variants_ls: KnechtVariantList):
        self.variants_ls = variants_ls

    @Slot(str)
    def send_command(self, command):
        self.command_ls.append(command)

    @Slot(dict)
    def set_options(self, knecht_dg_settings: dict):
        """ Update the options for the current send operation

        :param KnechtSettings.dg knecht_dg_settings: DeltaGen Settings attribute of KnechtSettings class
        """
        self.freeze_viewer: bool = knecht_dg_settings.get('freeze_viewer')
        self.check_variants: bool = knecht_dg_settings.get('check_variants')
        self.send_camera_data: bool = knecht_dg_settings.get('send_camera_data')
        self.long_render_timeout: bool = knecht_dg_settings.get('long_render_timeout')
        self.display_check: bool = knecht_dg_settings.get('display_variant_check')
        self.viewer_size: str = knecht_dg_settings.get('viewer_size')

    @Slot()
    def start_send_operation(self):
        self.abort_connection = False
        self.send_operation_in_progress = True

    @Slot()
    def abort(self):
        self.abort_connection = True
        LOGGER.info('Abort Signal triggered. Telling send thread to abort.')

    def restore_viewer(self):
        try:
            self.nc.send('SIZE VIEWER ' + self.viewer_size + '; UNFREEZE VIEWER;')
        except Exception as e:
            LOGGER.error('Sending viewer freeze command failed. %s', e)

    def exit_send_operation(self, result: int, skip_viewer: bool = False):
        if self.freeze_viewer and not skip_viewer and not self.rendering_mode:
            self.restore_viewer()

        self.nc.close()
        self.send_finished.emit(result)
        self.send_operation_in_progress = False

    def _connect_to_deltagen(self, timeout=3, num_tries=5):
        """ Tries to establish connection to DeltaGen in num_tries with increasing timeout """
        self.nc.connect()

        if self.rendering_mode:
            timeout, num_tries = 15, 6

        for c in range(0, num_tries):
            if self.nc.deltagen_is_alive(timeout):
                if self.send_operation_in_progress:  # Do not display on command operations
                    self.status.emit(_('DeltaGen Verbindung erfolgreich verifiziert.'))
                return True

            # Next try with slightly longer timeout
            LOGGER.error('Send to DeltaGen thread could not establish a connection after %s seconds.', timeout)
            timeout += c * 2

            if c == num_tries - 1:
                break

            for d in range(6, 0, -1):
                # Check abort signal
                if self.abort_connection:
                    return False

                self.status.emit(_('DeltaGen Verbindungsversuch ({!s}/{!s}) in {!s} Sekunden...')
                                 .format(c + 1, num_tries - 1, d - 1))
                time.sleep(1)

        # No DeltaGen connection, abort
        self.exit_send_operation(DeltaGenResult.send_failed)

        return False

    def _send_command_operation(self, command: str):
        timeout, num_tries = 2, 1

        if self.rendering_mode:
            timeout, num_tries = 20, 5

        if not self._connect_to_deltagen(timeout, num_tries):
            self.no_connection.emit()
            self.exit_send_operation(DeltaGenResult.cmd_failed, skip_viewer=True)
            return

        try:
            self.nc.send(command)
        except Exception as e:
            LOGGER.error('Sending command failed. %s', e)

        self.exit_send_operation(DeltaGenResult.cmd_success, skip_viewer=True)

    def _send_operation(self):
        self.status.emit(_('Prüfe Verbindung...'))

        if not self._connect_to_deltagen():
            self.no_connection.emit()
            self.exit_send_operation(DeltaGenResult.send_failed)
            return

        if self.freeze_viewer:
            self.status.emit(_('Sperre Viewer Fenster'))
            try:
                self.nc.send('SIZE VIEWER 320 240; FREEZE VIEWER;')
            except Exception as e:
                LOGGER.error('Sending freeze_viewer freeze command failed. %s', e)

        # Subscribe to variant states
        self.nc.send('SUBSCRIBE VARIANT_STATE;')

        # Abort signal
        if self.abort_connection:
            self.exit_send_operation(DeltaGenResult.aborted)
            return

        # Send variants
        for idx, variant in enumerate(self.variants_ls.variants):
            time.sleep(0.001)
            self._send_and_check_variant(variant, idx)

            # Abort signal
            if self.abort_connection:
                self.exit_send_operation(DeltaGenResult.aborted)
                return

        self.exit_send_operation(DeltaGenResult.send_success)

    def _send_and_check_variant(self, variant: KnechtVariant, idx, variants_num: int=0):
        """
            Send variant switch command and wait for variant_state EVENT
            variant: VARIANT SET STATE; as string
            idx: List index as integer, identifies the corresponding item in self.variants in thread class
        """
        # Update taskbar progress
        if not variants_num:
            variants_num = len(self.variants_ls)

        __p = round(100 / variants_num * (1 + idx))
        self.progress.emit(__p)

        if variant.item_type == 'command':
            # Look-up new command variants
            variant_str = f'{variant.value};'
        elif variant.item_type == 'camera_command':
            if not self.send_camera_data:
                return
            variant_str = f'{variant.value};'
        else:
            # Extract variant set and value
            variant_str = 'VARIANT {} {};'.format(variant.name, variant.value)

        # Add a long timeout in front of every variant send
        receive_timeout = self._regular_receive_timeout
        if self.long_render_timeout:
            self.nc.deltagen_is_alive(20)
            receive_timeout = self._long_receive_timeout

        # Send variant command
        self.nc.send(variant_str)

        # Check variant state y/n
        recv_str = ''
        if self.check_variants:
            # Receive Variant State Feedback
            recv_str = self.nc.receive(receive_timeout, log_empty=False)

        if recv_str:
            # Feedback should be: 'EVENT variant_state loaded_scene_name variant_idx'
            variant_recv_set, variant_recv_val = '', ''

            # Split into: ['EVENT variant_state scene2 ', 'variant_set', ' ', 'variant_state', '']
            recv_str = recv_str.split('"', 4)
            if len(recv_str) >= 4:
                variant_recv_set = recv_str[1]
                variant_recv_val = recv_str[3]

            # Compare if Feedback matches desired variant state
            if variant_recv_set in variant.name:
                variant.set_name_valid()

            if variant_recv_val in variant.value:
                variant.set_value_valid()

        # Signal results: -index in list-, set column, value column
        self.variant_status.emit(variant)
class KnechtImageViewerDgSync(QObject):
    set_btn_enabled_signal = pyqtSignal(bool)
    set_btn_checked_signal = pyqtSignal(bool)
    activate_viewer_window = pyqtSignal()

    viewer_name_wildcard = '.* \[Camer.*\]$'  # Looking for window with name Scene_Name * [Camera]

    def __init__(self, viewer):
        """ Worker object to sync DG Viewer to Image Viewer position and size

        :param KnechtImageViewer viewer: Image viewer parent
        """
        super(KnechtImageViewerDgSync, self).__init__()
        self.viewer = viewer
        self.dg_window = Win32WindowMgr()

        self.dg_btn_timeout = QTimer()
        self.dg_btn_timeout.setInterval(800)
        self.dg_btn_timeout.setSingleShot(True)
        self.dg_btn_timeout.timeout.connect(self.dg_reset_btn)

        self.dg_poll_timer = QTimer()
        self.dg_poll_timer.setInterval(600)
        self.dg_poll_timer.timeout.connect(self.dg_set_viewer)

        self.dg_viewer_pull_timer = QTimer()
        self.dg_viewer_pull_timer.setInterval(1500)
        self.dg_viewer_pull_timer.setSingleShot(True)
        self.dg_viewer_pull_timer.timeout.connect(self.viewer_pull_window)

        self.sync_dg = False
        self.pull_viewer_foreground = False
        self.pull_viewer_on_sync_start = True

        self.ncat = Ncat(TCP_IP, TCP_PORT)
        self.ncat.signals.recv_end.connect(self.viewer_pull_window)

        self.set_btn_enabled_signal.connect(self.viewer.dg_toggle_btn)
        self.set_btn_checked_signal.connect(self.viewer.dg_check_btn)

    def dg_reset_btn(self):
        self.set_btn_enabled_signal.emit(True)

    def dg_set_viewer(self):
        self.ncat.check_connection()

        position = f'{self.viewer.frameGeometry().x()} {self.viewer.frameGeometry().y()}'
        size = f'{self.viewer.size().width()} {self.viewer.size().height()}'
        command = f'UNFREEZE VIEWER;BORDERLESS VIEWER TRUE;SIZE VIEWER {size};POSITION VIEWER {position};'

        try:
            self.ncat.send(command)
        except Exception as e:
            LOGGER.error('Sending viewer size command failed. %s', e)

        if not self.dg_viewer_pull_timer.isActive():
            self.dg_viewer_pull_timer.start()

        if not self.pull_viewer_foreground:
            self.dg_poll_timer.stop()

    def dg_reset_viewer(self):
        self.dg_poll_timer.stop()

        self.ncat.check_connection()
        try:
            self.ncat.send('BORDERLESS VIEWER FALSE;')
        except Exception as e:
            LOGGER.error('Sending viewer size command failed. %s', e)

        self.dg_reset_btn()

    def dg_close_connection(self):
        self.dg_viewer_pull_timer.stop()
        self.dg_poll_timer.stop()

        if self.sync_dg:
            self.dg_reset_viewer()
            self.ncat.close()

    @pyqtSlot()
    def dg_start_sync(self):
        """ Image Viewer Window <> DeltaGen Viewer sync requested """
        if self.sync_dg:
            if not self.dg_poll_timer.isActive():
                self.dg_poll_timer.start()

    @pyqtSlot()
    def dg_toggle_sync(self):
        self.sync_dg = not self.sync_dg
        self.set_btn_checked_signal.emit(self.sync_dg)
        self.set_btn_enabled_signal.emit(False)
        self.dg_btn_timeout.start()

        if self.sync_dg:
            if self.ncat.deltagen_is_alive():
                self.dg_start_sync()
                self.viewer_clear()
                self.viewer_window_find()
            else:
                self.dg_toggle_sync()  # No connection, toggle sync off
        else:
            self.dg_reset_viewer()

    @pyqtSlot(bool)
    def viewer_toggle_pull(self, enabled: bool):
        LOGGER.debug('Setting pull_viewer_foreground: %s', enabled)
        self.pull_viewer_foreground = enabled

        if self.sync_dg:
            self.dg_poll_timer.start()

    def viewer_clear(self):
        self.dg_window.clear_handle()

    def viewer_window_find(self):
        """ Tries to find the MS Windows window handle and pulls the viewer window to foreground """
        try:
            self.dg_window.find_window_wildcard(self.viewer_name_wildcard)

            self.pull_viewer_on_sync_start = True
        except Exception as e:
            LOGGER.error('Error finding DeltaGen Viewer window.\n%s', e)

    def viewer_pull_window(self):
        # Pull DeltaGen Viewer to foreground
        if not self.pull_viewer_foreground and not self.pull_viewer_on_sync_start:
            return

        if not self.dg_window.has_handle():
            self.viewer_window_find()

        try:
            self.dg_window.set_foreground()
            LOGGER.debug('Pulling viewer window to foreground.')
            self.activate_viewer_window.emit()
        except Exception as e:
            LOGGER.error('Error setting DeltaGen Viewer foreground.\n%s', e)

        # Initial pull done, do not pull to front on further sync
        self.pull_viewer_on_sync_start = False
示例#4
0
class send_to_dg_worker(QObject):
    finished = pyqtSignal()
    no_connection = pyqtSignal()
    status = pyqtSignal(str)
    display_msg = pyqtSignal(str, object)
    strReady = pyqtSignal(object, object, object)
    render_progress = pyqtSignal(int)
    task_progress = pyqtSignal(int)

    green_on = pyqtSignal()
    green_off = pyqtSignal()
    yellow_on = pyqtSignal()
    yellow_off = pyqtSignal()

    def __init__(self, variants_list, viewer, check_variants, render_dict=dict(), render_user_path=False,
                 convert_to_png=True, long_render_timeout=False, create_render_preset_dir=False):
        super(QObject, self).__init__()
        self.variants_list = variants_list
        self.render_dict = render_dict
        # Freeze viewer during send *bool
        self.viewer = viewer
        self.viewer_size = SendToDeltaGen.viewer_size
        self.check_variants = check_variants
        self.convert_to_png = convert_to_png
        self.abort_connection = False
        self.render_user_path = render_user_path
        self.long_render_timeout = long_render_timeout
        self.create_render_preset_dir = create_render_preset_dir
        self.nc = Ncat(TCP_IP, TCP_PORT)

        # Connect NC signals to LED's
        self.nc.signals.send_start.connect(self.green_on)
        self.nc.signals.send_end.connect(self.green_off)
        self.nc.signals.recv_start.connect(self.yellow_on)
        self.nc.signals.recv_end.connect(self.yellow_off)
        self.nc.signals.connect_start.connect(self.yellow_on)
        self.nc.signals.connect_end.connect(self.yellow_off)

    @pyqtSlot()
    def abort_signal(self):
        self.abort_connection = True
        LOGGER.info('Abort Signal triggered. Telling send thread to abort.')

    def exit_thread(self):
        if self.viewer:
            self.restore_viewer()

        self.abort_connection = True
        self.nc.close()
        self.finished.emit()

    def connect_to_deltagen(self, timeout=3, num_tries=5):
        """ Tries to establish connection to DeltaGen in num_tries with increasing timeout """
        self.nc.connect()

        for c in range(0, num_tries):
            dg_connected = self.nc.deltagen_is_alive(timeout)

            if dg_connected:
                self.display_msg.emit('DeltaGen Verbindung erfolgreich verifiziert.', ())
                return True

            # Next try with slightly longer timeout
            timeout += c * 2

            LOGGER.error('Send to DeltaGen thread could not establish a connection after %s seconds.', timeout)

            if c == num_tries - 1:
                break

            for d in range(6, 0, -1):
                # Check abort signal
                QtWidgets.QApplication.processEvents()
                if self.abort_connection:
                    return False

                self.display_msg.emit(
                    'DeltaGen Verbindungsversuch ({!s}/{!s}) in <b>{!s}</b> Sekunden...'.format(c + 1, num_tries - 1,
                                                                                                d - 1), ())

                time.sleep(1)

        # No DeltaGen connection, abort
        self.display_msg.emit('Konnt keine Verbindung zu einer DeltaGen Instanz mit geladener Szene herstellen.',
                              ('Tja', None))

        self.nc.close()

        return False

    @pyqtSlot()
    def send_variants(self):  # A slot takes no params
        # Connection check timeout, can take a while when GI or RT is active
        timeout = 3
        if self.render_dict:
            timeout = 20

        self.status.emit('Prüfe Verbindung...')

        if not self.connect_to_deltagen(timeout):
            if self.abort_connection:
                self.exit_thread()
                return

            self.no_connection.emit()
            self.finished.emit()
            return

        if self.viewer:
            self.status.emit('Viewer freeze...')
            try:
                self.nc.send('SIZE VIEWER 320 240; FREEZE VIEWER;')
            except:
                LOGGER.error('Sending viewer freeze command failed.')

        # Subscribe to variant states
        self.nc.send('SUBSCRIBE VARIANT_STATE;')

        if self.render_dict:
            self.status.emit('Beginne Rendering...')
            self.render_loop()

            # Abort signal
            QtWidgets.QApplication.processEvents()
            if self.abort_connection:
                self.exit_thread()
                return
        else:
            # Abort signal
            QtWidgets.QApplication.processEvents()
            if self.abort_connection:
                self.exit_thread()
                return

            # Send variants
            for idx, variant in enumerate(self.variants_list):
                time.sleep(0.001)
                self.send_and_check_variant(variant, idx)

                # Abort signal
                QtWidgets.QApplication.processEvents()
                if self.abort_connection:
                    self.exit_thread()
                    return

        self.exit_thread()

    def restore_viewer(self):
        try:
            self.nc.send('SIZE VIEWER ' + self.viewer_size + '; UNFREEZE VIEWER;')
        except:
            LOGGER.error('Sending viewer freeze command failed.')

    def send_and_check_variant(self, variant, idx, variants_num: int=0):
        """
            Send variant switch command and wait for variant_state EVENT
            variant: VARIANT SET STATE; as string
            idx: List index as integer, identifies the corresponding item in self.variants_list in thread class
        """
        self.status.emit('Schaltung wird gesendet...')

        # Update taskbar progress
        if not variants_num:
            variants_num = len(self.variants_list)
        __p = round(100 / variants_num * (1 + idx))
        self.task_progress.emit(__p)

        # Extract variant set and value
        var_split = variant.split(' ', 2)
        if len(var_split) == 3:
            variant_set = var_split[1]
            variant_value = var_split[2]
        else:
            LOGGER.error('Invalid variant will be skipped: %s Index: %s', variant, idx)
            return

        # Index of Item in variants_list
        var_idx = idx

        # Extra feedbackloop
        if self.long_render_timeout:
            self.nc.deltagen_is_alive(20)

        # Send variant command
        self.nc.send(variant)

        # Check variant state y/n
        if self.check_variants:
            if self.long_render_timeout:
                recv_str = self.nc.receive(2)
            else:
                # Receive Variant State Feedback
                recv_str = self.nc.receive()
        else:
            recv_str = None

        if recv_str is None:
            recv_str = ''

        # Feedback should be: 'EVENT variant_state loaded_scene_name variant_idx'
        if recv_str:
            # Split into: ['EVENT variant_state scene2 ', 'variant_set', ' ', 'variant_state', '']
            recv_str = recv_str.split('"', 4)
            if len(recv_str) >= 4:
                variant_recv_set = recv_str[1]
                variant_recv_val = recv_str[3]
            else:
                variant_recv_set = ''
                variant_recv_val = ''

            # Compare if Feedback matches desired variant state
            if variant_recv_set in variant_set:
                # Set column to set to green
                variant_set = 1
            else:
                variant_set = False

            if variant_recv_val in variant_value:
                # Set column to set to green
                variant_value = 2
            else:
                variant_value = False
        else:
            variant_set, variant_value = False, False

        # Signal results: -index in list-, set column, value column
        self.strReady.emit(var_idx, variant_set, variant_value)

    @staticmethod
    def return_time(only_minutes=False):
        date_msg = time.strftime('%Y-%m-%d')
        time_msg = time.strftime('%H:%M:%S')

        if only_minutes:
            return time_msg
        else:
            return date_msg + ' ' + time_msg

    def create_directory(self, dir, fallback_name):
        dir = Path(dir)

        if not dir.exists():
            try:
                dir.mkdir(parents=True)
            except:
                LOGGER.critical('Could not create rendering directory! Rendering to executable path.')
                dir = HELPER_DIR.parents[0] / fallback_name
                dir = dir.absolute()

        return dir

    def init_render_log(self):
        self.render_log_name = 'RenderKnecht_Log_' + str(time.time()) + '.log'
        self.render_log = ''
        self.render_log += Msg.RENDER_LOG[0] + self.return_time() + '\n\n'

    def render_loop(self):
        """ Render Loop """
        # List of paths to rendered img files
        self.img_list = []
        img_count = 0
        self.init_render_log()

        # Render Path
        out_dir_name = 'out_' + str(time.time())
        self.out_dir = HELPER_DIR.parents[0] / out_dir_name

        if self.render_user_path:
            self.out_dir = self.render_user_path / out_dir_name

        self.out_dir = self.out_dir.absolute()
        self.out_dir = self.create_directory(self.out_dir, out_dir_name)
        self.initial_out_dir = self.out_dir
        LOGGER.info('Output Directory: %s', self.out_dir)

        # Display render path in overlay
        self.strReady.emit(-1, Msg.OVERLAY_RENDER_DIR, str(self.out_dir))

        # Iterate Render Presets's
        for r in range(0, len(self.render_dict.items())):
            sampling = self.render_dict[r].get('sampling')
            resolution = self.render_dict[r].get('resolution')
            file_extension = self.render_dict[r].get('file_extension')
            render_preset_name = self.render_dict[r]['render_preset_name']

            # Create Render Preset Output Directory
            if self.create_render_preset_dir:
                render_preset_name = to_valid_chrs(render_preset_name)

                new_out_dir = self.initial_out_dir / render_preset_name
                self.out_dir = self.create_directory(new_out_dir, out_dir_name)
                LOGGER.debug('Created Render Preset directory: %s', self.out_dir.name)

            try:
                samples = str(2 ** int(sampling))
                self.render_log += render_preset_name + ' Einstellungen - '
                self.render_log += 'Sampling: 2^' + sampling + ' ' + samples + ' - Res: '
                self.render_log += resolution.replace(' ', 'x') + 'px - Ext: ' + file_extension + '\n\n'
            except:
                pass

            # Make sure we render even if no viewset supplied
            if self.render_dict[r]['viewsets'] == []:
                self.render_dict[r]['viewsets'].append('Dummy')

            self.render_start_time = time.time()

            # Render Preset preset's / reference's (one image per reference)
            for preset in self.render_dict[r]['preset'].items():
                # unpack tuple
                idx, preset = preset

                # Iterate Render Preset viewset's
                for viewset in self.render_dict[r]['viewsets']:
                    # Ascend image number for all images in all Render Preset's
                    img_count += 1

                    # Call render method
                    self.render_preset(img_count, preset, viewset, sampling, resolution, file_extension)

                    # Abort signal
                    QtWidgets.QApplication.processEvents()
                    if self.abort_connection: return

            if self.create_render_preset_dir:
                try:
                    with open(self.out_dir / self.render_log_name, 'w') as e:
                        print(self.render_log, file=e)
                    self.init_render_log()
                except:
                    pass

        # Convert rendered images
        self.status.emit('Konvertiere Bilddaten...')
        if self.convert_to_png and self.img_list:
            self.render_log += create_png_images(self.img_list, self.create_render_preset_dir)

        # Create log file after rendering complete
        try:
            with open(self.initial_out_dir / self.render_log_name, 'w') as e:
                print(self.render_log, file=e)
        except Exception as e:
            LOGGER.error('Error saving render log file: %s', e)

    def render_preset(self, img_count, preset, viewset, sampling, resolution, file_extension):
        """ Sub loop, switch variants and render current preset """
        # Make sure we are not assigning objects with +=
        name = preset.get('name')
        variant_list = []
        variant_list += preset.get('variants')

        # Viewset name if supplied as "Variant Viewset View;" else will return ''
        viewset_name = get_viewset_name(viewset)

        # Append viewset variant if name and therefore a valid viewset variant was supplied
        if viewset_name:
            variant_list.append(viewset)

        # Output Image Name
        img_name = '{:03d}_{name}{viewset}{ext}'.format(img_count, name=name, viewset=viewset_name, ext=file_extension)

        # Replace invalid file name characters
        img_name = to_valid_chrs(img_name)

        LOGGER.info('Rendering: %s\nAA: %s RES: %s EXT: %s', img_name, sampling, resolution, file_extension)
        self.render_log += self.return_time() + ' ' + Msg.RENDER_LOG[1] + img_name + '\n' + Msg.RENDER_LOG[2]

        # Send variants
        for idx, variant in enumerate(variant_list):
            time.sleep(0.001)
            self.send_and_check_variant(variant, idx, len(variant_list))

            self.render_log += variant.replace('VARIANT ', '')

            # Abort signal
            QtWidgets.QApplication.processEvents()
            if self.abort_connection: return

        self.render_log += '\n\n'

        time.sleep(0.1)
        # Send settings command
        self.nc.send('IMAGE_SAA_QUALITY VIEWER ' + sampling)

        # Rendering command
        time.sleep(0.1)
        img_file_path = self.out_dir / img_name

        # Build img list for conversion
        self.img_list.append(img_file_path)

        # Feedbackloop before render command
        if self.long_render_timeout:
            self.nc.close()
            time.sleep(1)
            # Subscribe to variant states
            self.nc.send('SUBSCRIBE VARIANT_STATE;')
            time.sleep(1)

        self.nc.deltagen_is_alive(20)

        # Render command
        self.nc.send('IMAGE "' + str(img_file_path) + '" ' + str(resolution) + ';')

        # Calculate render time
        if img_count == 1: self.render_start_time = time.time()
        render_time, image_num = self.calc_render_time(self.render_dict)

        # Wait until image was created
        while not img_file_path.exists():
            time.sleep(1)
            render_display = self.calculate_remaining(render_time, img_count, image_num)
            self.status.emit('Rendering ' + render_display)

            QtWidgets.QApplication.processEvents()
            if self.abort_connection: return

        # Verify a valid image file was created
        self.status.emit('Prüfe Bilddaten...')
        self.verify_rendered_image(img_file_path)

        # Image created
        self.status.emit('Rendering erzeugt.')
        time.sleep(0.5)

        # Wait 5 seconds for DeltaGen to recover
        for count in range(2, 0, -1):
            self.status.emit('Erzeuge nächstes Bild in ' + str(count) + '...')
            time.sleep(1)

    def verify_rendered_image(self, img_path, timeout=3300):
        """ Read rendered image with ImageIO to verify as valid image or break after 55mins/3300secs """
        begin = time.time()
        img = False
        exception_message = ''

        if self.long_render_timeout:
            # Long render timeout eg. A3 can take up to 40min to write an image
            # wait for 30min / 1800sec
            timeout = 1800

        while 1:
            QtWidgets.QApplication.processEvents()
            if self.abort_connection: return

            try:
                # Try to read image
                img = imread(str(img_path))
                img = True
            except ValueError or OSError as exception_message:
                """ Value error if format not found or file incomplete; OSError on non-existent file """
                if time.time() - begin < 11:
                    LOGGER.debug('Rendered image could not be verified. Verification loop %s sec.\n%s', timeout,
                                 exception_message)

                # Display image verification in Overlay
                try:
                    msg = Msg.OVERLAY_RENDER_IMG_ERR + str(img_path.name)
                    self.display_msg.emit(msg, ())
                except Exception as e:
                    LOGGER.error('Tried to send overlay message. But:\n%s', e)

                QtWidgets.QApplication.processEvents()
                # Wait 10 seconds
                time.sleep(10)

            if img:
                del img
                LOGGER.debug('Rendered image was verified as valid image file.')

                # Display image verification in Overlay
                try:
                    msg = Msg.OVERLAY_RENDER_IMG + str(img_path.name)
                    self.display_msg.emit(msg, ())
                except Exception as e:
                    LOGGER.error('Tried to send overlay error message. But:\n%s', e)

                break

            # Timeout
            if time.time() - begin > timeout:
                LOGGER.error('Rendered image could not be verified as valid image file after %s seconds.', timeout)
                self.render_log += '\nDatei konnte nicht als gültige Bilddatei verfiziert werden: ' + str(
                    img_path) + '\n'

                try:
                    if exception_message:
                        self.render_log += exception_message + '\n'
                except UnboundLocalError:
                    # exception_message not defined
                    pass

                break

    def calculate_remaining(self, render_time, img_count, image_num):
        """ Returns remaining time in hh: mm: ss """
        # If image rendered faster than estimated, show progress in progress bar
        elapsed_delta = 0
        render_time = int(render_time)

        if img_count > 1:
            elapsed_delta = (render_time / max(1, image_num)) * (img_count - 1)

        # Render time passed by
        render_seconds_elapsed = int(time.time() - self.render_start_time)
        # Remaining time in seconds
        render_seconds_remaining = max(0, render_time - render_seconds_elapsed)

        # Update Progress bar
        progress = max(render_seconds_elapsed, elapsed_delta) * 100 / max(1, render_time)
        progress = min(100, max(1, progress))
        self.render_progress.emit(int(progress))

        # 0h:00min:00sec
        return time_string(render_seconds_remaining)

    @staticmethod
    def calc_render_time(render_dict):
        """ Calculate render time in seconds """
        render_time = 0
        image_num_all = 0

        for r in range(0, len(render_dict.items())):
            # Sampling
            sampling = 2 ** int(render_dict[r].get('sampling'))

            # Resolution X
            resolution = render_dict[r].get('resolution')
            res_x = 0
            if len(resolution.split(' ')) >= 1:
                res_x = int(resolution.split(' ')[0])

            # Number of viewsets
            if render_dict[r]['viewsets'] == []:
                viewset_num = 1
            else:
                viewset_num = 0
                for viewset in render_dict[r]['viewsets']:
                    viewset_num += 1

            # Number of presets
            preset_num = 0
            for idx, preset in render_dict[r]['preset'].items():
                preset_num += 1
            preset_num = max(1, preset_num)
            image_num = viewset_num * preset_num
            image_num_all += image_num

            sampling_factor = res_x * sampling
            resolution_factor = res_x * RENDER_RES_FACTOR
            render_preset_time = sampling_factor * RENDER_MACHINE_FACTOR * resolution_factor
            render_preset_time = render_preset_time * image_num
            render_time += render_preset_time

        return render_time, image_num_all