"""Helpers for GUI-side runtime history management.""" from __future__ import annotations from collections import deque from dataclasses import astuple from python_app.models.collection_identity import collection_key from python_app.models.dataset_model import ResultCollection from python_app.models.run_config_schema import RunConfigModel from python_app.orchestration.preprocess_assets import PREPROCESS_ASSET_NAMES, preprocess_asset_model def record_result_history( result_history: deque[ResultCollection], collection: ResultCollection, ) -> bool: """Record a result, keeping the history in acquisition order; return whether it is new. A result of a sweep already present replaces it (the sweep was reprocessed). Any other result goes after the newest older one: normally at the end, as the result of a new sweep, but the processor reprocessing its retained sweeps also delivers sweeps whose first result the GUI missed, and appending those would show them out of order. """ if result_history.maxlen == 0: return False key = collection_key(collection) position = len(result_history) while position > 0: existing = result_history[position - 1] if collection_key(existing) == key: result_history[position - 1] = collection return False if existing.capture_start_ns < collection.capture_start_ns: break position -= 1 if position == len(result_history): result_history.append(collection) return True if len(result_history) == result_history.maxlen: if position == 0: return False # older than everything kept, so it would be the first to go result_history.popleft() position -= 1 result_history.insert(position, collection) return True def build_processor_run_signature( config: RunConfigModel, ) -> tuple[object, ...]: """Settings consumed at processor startup, rather than through the live file. Excludes the preprocess set names on purpose: the data_processor does not consume calibration/reference sets (those are applied upstream by the data_preprocessor), so changing a set must NOT restart the processor. Restarting it would also tear down the locator TCP server it hosts and drop every connected client. Shape, ring endpoints, locator settings, live-file path and polling interval require a restart. """ combos_signature = tuple((int(combo.input), int(combo.output)) for combo in config.combos) return ( config.radar_key(), config.is_multi_device and config.radar.multi_device.force_external_reference, str(config.radar.driver_mode), str(config.input_switch.driver_mode), int(config.input_switch.positions), str(config.output_switch.driver_mode), int(config.output_switch.positions), combos_signature, astuple(config.rings), astuple(config.runtime.locator_server), str(config.runtime.processing_live_config_path), int(config.runtime.idle_sleep_ms), ) def build_run_history_signature( config: RunConfigModel, ) -> tuple[object, ...]: """Reset display history when startup settings or preprocessing change. Display history still resets when the reference/calibration set changes (the on-screen B-scan would otherwise mix old- and new-reference frames), even though the processor process itself is intentionally kept alive across that change. """ preprocess_signature = tuple(astuple(preprocess_asset_model(config, key)) for key in PREPROCESS_ASSET_NAMES) notch = config.preprocess.notch notch_signature = ( bool(notch.enabled), tuple(tuple(band) for band in notch.bands_hz), float(notch.taper_width_hz), str(notch.taper_type), ) return build_processor_run_signature(config) + (preprocess_signature, notch_signature)