diff --git a/apps/vertex2.py b/apps/vertex2.py index ea84ed6..7bdae80 100644 --- a/apps/vertex2.py +++ b/apps/vertex2.py @@ -15,9 +15,10 @@ class Vertex2(SmartObject): self._stop_interrupt_listener() self._interrupt_stop = threading.Event() self._status_requested = threading.Event() - self._tx0_recovery_attempted = False - self._tx0_recovery_restore_handle = None - + self._plus5_off_handle = None + self._plus5_active_index = None + self._input_signal_present = False + self.plus5_signal_timeout = max(0, float(self.args.get('plus5_signal_timeout', 60))) self.video_tx_signal_sensor = self.create_entity( self.args['video_tx_signal_sensor'], state='unavailable', @@ -175,21 +176,12 @@ class Vertex2(SmartObject): buffer = '' status = {} status_started = None - interrupt_generation = 0 - last_interrupt_at = None - last_interrupt_interval = None - request_generation = 0 - request_interval = None - previous_snapshot = None - previous_snapshot_generation = None while not interrupt_stop.is_set(): if status_started is None and self._status_requested.is_set(): self._status_requested.clear() status = {} status_started = time.monotonic() - request_generation = interrupt_generation - request_interval = last_interrupt_interval for command in commands: connection.sendall((command + '\r').encode('ascii')) @@ -214,10 +206,6 @@ class Vertex2(SmartObject): line = line.strip() if line == 'int': self.log_info('Vertex2 IP INTERRUPT received') - now = time.monotonic() - last_interrupt_interval = None if last_interrupt_at is None else now - last_interrupt_at - last_interrupt_at = now - interrupt_generation += 1 self._status_requested.set() elif status_started is not None: if line.startswith('insel '): @@ -236,23 +224,6 @@ class Vertex2(SmartObject): tx0=snapshot['tx0'], tx1=snapshot['tx1'], audout=snapshot['audout'], insel=snapshot['insel']) status_started = None - consecutive = (previous_snapshot_generation is not None - and request_generation == previous_snapshot_generation + 1) - unchanged = previous_snapshot == snapshot - rx_has_signal = any( - snapshot[field].strip().lower() not in ('', 'no signal', 'unavailable') - for field in ('rx0', 'rx1') - ) - if (unchanged and consecutive and request_interval is not None - and 2 <= request_interval < 3 and not rx_has_signal): - if not self._tx0_recovery_attempted: - self.run_in(self.trigger_tx0_stuck_workaround, 0, - interrupt_interval=request_interval, - snapshot=snapshot) - elif not unchanged: - self._tx0_recovery_attempted = False - previous_snapshot = snapshot - previous_snapshot_generation = request_generation if status_started is not None and time.monotonic() - status_started > 5: raise OSError('Vertex2 TCP status response timed out') @@ -271,30 +242,6 @@ class Vertex2(SmartObject): self.log_warning(f'Vertex2 IP INTERRUPT unavailable: {error}') interrupt_stop.wait(5) - def trigger_tx0_stuck_workaround(self, kwargs): - if self._tx0_recovery_attempted: - return - if self.output_selected_sensor.get_state() != self.get_output_config(0)['name']: - return - if self.unreachable_sensor.get_state() != 'off': - return - - self._tx0_recovery_attempted = True - snapshot_summary = ', '.join(f'{key}={value!r}' for key, value in kwargs['snapshot'].items()) - self.log_warning( - f"Vertex2 TX0 +5V workaround triggered: identical complete TCP status after " - f"an IP INTERRUPT interval of {kwargs['interrupt_interval']:.2f}s " - f"({snapshot_summary}); cycling TX0 +5V FORCE" - ) - self.force_tx0_plus5(False) - self._tx0_recovery_restore_handle = self.run_in(self.restore_tx0_plus5_after_recovery, 1) - - def restore_tx0_plus5_after_recovery(self, kwargs): - self._tx0_recovery_restore_handle = None - if self.output_selected_sensor.get_state() == self.get_output_config(0)['name']: - self.log_info('Vertex2 TX0 +5V workaround: restoring TX0 +5V FORCE') - self.force_tx0_plus5(True) - def _stop_interrupt_listener(self): interrupt_stop = getattr(self, '_interrupt_stop', None) if interrupt_stop is not None: @@ -306,13 +253,15 @@ class Vertex2(SmartObject): if interrupt_thread.is_alive(): raise RuntimeError('Previous Vertex2 IP INTERRUPT listener did not stop') self._interrupt_thread = None - restore_handle = getattr(self, '_tx0_recovery_restore_handle', None) - if restore_handle is not None: - self.cancel_timer(restore_handle) - self._tx0_recovery_restore_handle = None + plus5_off_handle = getattr(self, '_plus5_off_handle', None) + if plus5_off_handle is not None: + self.cancel_timer(plus5_off_handle) + self._plus5_off_handle = None def terminate(self): self._stop_interrupt_listener() + if getattr(self, '_plus5_active_index', None) is not None: + self._set_active_output_plus5(None) super().terminate() def generate_input_selected_sensor_name(self,i): @@ -394,13 +343,10 @@ class Vertex2(SmartObject): self.log_info(f"Selecting TX{output_index} as the active output") self.apply_output_audio_settings(output_index) if output_index == 0: - self.force_tx1_plus5(False) self.set_edid_video_flags_to_tx0() - self.force_tx0_plus5(True) else: - self.force_tx0_plus5(False) self.set_edid_video_flags_to_tx1() - self.force_tx1_plus5(True) + self._set_active_output_plus5(output_index if self._input_signal_present else None) def disable_output_selection(self): default_output_config = self.get_output_config(None) @@ -409,8 +355,60 @@ class Vertex2(SmartObject): attributes={"icon": default_output_config["icon"]}, ) self._refresh_selected_output_signal() - self.force_tx0_plus5(False) - self.force_tx1_plus5(False) + self._set_active_output_plus5(None) + + def _set_active_output_plus5(self, output_index): + if self._plus5_off_handle is not None: + self.cancel_timer(self._plus5_off_handle) + self._plus5_off_handle = None + + previous_index = self._plus5_active_index + if previous_index == output_index: + return + + if previous_index == 0: + self.force_tx0_plus5(False) + elif previous_index == 1: + self.force_tx1_plus5(False) + + self._plus5_active_index = output_index + if output_index == 0: + # At startup the previous hardware state is unknown; disable the + # other output only after a signal confirms the selected TX. + self.force_tx1_plus5(False) + self.force_tx0_plus5(True) + elif output_index == 1: + self.force_tx0_plus5(False) + self.force_tx1_plus5(True) + + def _update_plus5_from_input_signal(self, has_signal): + self._input_signal_present = has_signal + selected_name = self.output_selected_sensor.get_state() + selected_index = self.output_names.get(selected_name) + + if has_signal: + self._set_active_output_plus5(selected_index) + elif selected_index in (0, 1) and self._plus5_off_handle is None: + self.log_info( + f"No input signal; turning selected TX +5V FORCE off in {self.plus5_signal_timeout:g}s" + ) + self._plus5_off_handle = self.run_in( + self._turn_off_plus5_after_signal_loss, + self.plus5_signal_timeout, + ) + + def _turn_off_plus5_after_signal_loss(self, kwargs): + self._plus5_off_handle = None + if self._input_signal_present: + return + self.log_info("Input signal absent for configured timeout; turning selected TX +5V FORCE off") + selected_index = self.output_names.get(self.output_selected_sensor.get_state()) + if selected_index == 0: + self.force_tx0_plus5(False) + elif selected_index == 1: + self.force_tx1_plus5(False) + if self._plus5_active_index == selected_index: + self._plus5_active_index = None def on_filmmaker_mode_conditions_change(self,prev_result,result): if result == SmartCondition.Result.Succeeded: @@ -646,6 +644,11 @@ class Vertex2(SmartObject): selected_output_index = self.output_names.get(self.output_selected_sensor.get_state()) self._refresh_selected_input_sensor(selected_output_index) self._update_output_video_sensors(kwargs['rx0'], kwargs['rx1']) + has_input_signal = any( + signal.strip().lower() not in ('', 'no signal', 'unavailable') + for signal in (kwargs['rx0'], kwargs['rx1']) + ) + self._update_plus5_from_input_signal(has_input_signal) if self.audio_out_signal_sensor.get_state() != audio_out_signal: self.audio_out_signal_sensor.set_state(state=audio_out_signal) self.unreachable_sensor.set_state(state='off')