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 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
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