some fixes

This commit is contained in:
Ayzen
2026-06-04 18:33:38 +03:00
parent eacea436a4
commit 22942d9dc9
26 changed files with 1352 additions and 153 deletions
@@ -0,0 +1,90 @@
"""Mixin wiring a physical GPIO control button to a runtime action.
Both GUI and headless launches construct :class:`AppWindow`, so attaching the
watcher here makes the button behave identically in both modes. The button
reuses the existing "Capture Tmp Reference" flow (stop the pipeline, capture a
fresh tmp reference, then restart the pipeline if it had been running), so no
acquisition logic is duplicated for the hardware trigger.
"""
from __future__ import annotations
from python_app.gui.control_button import ControlButtonWatcher
from python_app.models.run_config_schema import ControlButtonModel
class AppWindowControlButtonMixin:
"""Start and stop a background GPIO button watcher bound to a runtime action."""
def _init_control_button_state(self) -> None:
"""Initialize the watcher handle before the watcher is started."""
self._control_button_watcher: ControlButtonWatcher | None = None
def _start_control_button_watcher(self) -> None:
"""Open the configured GPIO button line and begin watching for presses.
Any failure is logged and swallowed: the same build runs on developer
machines and non-Pi hosts where the GPIO chip is absent, and a missing
button must never abort startup.
"""
config = getattr(self._defaults_config, "control_button", None)
if config is None or not config.enabled:
return
if config.pin < 0 or not config.gpio_chip:
self._log_warning(
"Control button enabled but gpio_chip/pin are unset; watcher not started."
)
return
if config.action != ControlButtonModel.ACTION_CAPTURE_TMP_REFERENCE:
self._log_warning(
f"Control button action '{config.action}' is not supported; watcher not started."
)
return
try:
watcher = ControlButtonWatcher(
chip=config.gpio_chip,
pin=config.pin,
active_low=config.active_low,
bias=config.bias,
debounce_ms=config.debounce_ms,
parent=self,
)
watcher.pressed.connect(self._on_control_button_pressed)
watcher.failed.connect(self._on_control_button_failed)
watcher.start()
except Exception as exc: # noqa: BLE001
self._log_exception("Failed to start GPIO control button watcher", exc, level="WARN")
return
self._control_button_watcher = watcher
self._log(
"GPIO control button watcher started: "
f"chip={config.gpio_chip}, pin={config.pin}, active_low={config.active_low}, "
f"debounce_ms={config.debounce_ms}, action={config.action}"
)
def _on_control_button_pressed(self) -> None:
"""Run the configured action for a physical press on the Qt main thread.
Delivered as a queued signal from the watcher thread, so this executes
on the GUI thread exactly like a click on "Capture Tmp Reference".
"""
self._log("GPIO control button pressed: capturing tmp reference.")
self._capture_tmp_reference()
def _on_control_button_failed(self, message: str) -> None:
"""Log an unrecoverable watcher error reported from the background thread."""
self._log_warning("GPIO control button watcher stopped", details=message)
def _stop_control_button_watcher(self) -> None:
"""Stop the watcher and release its GPIO line during shutdown."""
watcher = getattr(self, "_control_button_watcher", None)
if watcher is None:
return
try:
watcher.stop()
except Exception as exc: # noqa: BLE001
self._log_warning("Error stopping GPIO control button watcher", details=str(exc))
finally:
self._control_button_watcher = None
@@ -6,7 +6,11 @@ import time
from python_app.gui.runtime.constraints import validate_processing_mode_constraints
from python_app.gui.runtime.history import build_run_history_signature, record_result_history
from python_app.gui.runtime.history import (
build_processor_run_signature,
build_run_history_signature,
record_result_history,
)
from python_app.hardware_full.kamil_adc_service import apply_kamil_adc_laser_control
from python_app.hardware_full.single_radar_service import create_single_radar_service
from python_app.models.dataset_model import ComboKey, ResultCollection, SweepCollection
@@ -57,7 +61,8 @@ class AppWindowPipelineMixin:
processor_was_running = self._supervisor.is_processor_running()
config = self._build_config()
run_signature = self._build_run_history_signature(config)
if self._processor_requires_restart(run_signature):
processor_signature = self._build_processor_run_signature(config)
if self._processor_requires_restart(processor_signature):
self._log("Restarting data_processor because stable run settings changed")
self._stop_all_processes()
processor_was_running = False
@@ -122,7 +127,7 @@ class AppWindowPipelineMixin:
self._raw_reader = ShmRingReader(config.rings.raw_tap.name)
self._pre_reader = ShmRingReader(config.rings.preprocessed_tap.name)
self._result_reader = ShmRingReader(config.rings.results.name)
self._processor_run_signature = run_signature
self._processor_run_signature = processor_signature
self._single_capture_active = single_capture
self._single_capture_start_ns = None
self._single_capture_seen_raw = False
@@ -167,8 +172,8 @@ class AppWindowPipelineMixin:
config = self._build_config()
if config.radar.driver_mode == "native":
self._refresh_radar_limits_from_device()
run_signature = self._build_run_history_signature(config)
if processor_only_running and self._processor_requires_restart(run_signature):
processor_signature = self._build_processor_run_signature(config)
if processor_only_running and self._processor_requires_restart(processor_signature):
self._stop_all_processes()
self._reset_runtime_history()
self._history_run_signature = None
@@ -492,9 +497,13 @@ class AppWindowPipelineMixin:
self._result_history.extend(result_tail)
def _build_run_history_signature(self, config: RunConfigModel) -> tuple[object, ...]:
"""Build signature used to decide when history should be reset."""
"""Build signature used to decide when display history should be reset."""
return build_run_history_signature(config)
def _build_processor_run_signature(self, config: RunConfigModel) -> tuple[object, ...]:
"""Build signature used to decide when the data_processor must be restarted."""
return build_processor_run_signature(config)
def _validate_processing_mode_constraints(self, config: RunConfigModel) -> None:
"""Validate processing-mode constraints for run start."""
validate_processing_mode_constraints(
@@ -142,11 +142,19 @@ class AppWindowPreprocessMixin:
)
self._selected_preprocess_sets["s21_reference"] = TMP_REFERENCE_SET_NAME
self._selected_preprocess_radar_key = radar_key
self._processor_run_signature = None
# Do NOT restart data_processor for a reference change: it does not consume
# the S21 reference (the data_preprocessor does), and restarting it would tear
# down the locator server it hosts and drop every connected client. The
# preprocessor reloads the new reference when acquisition restarts below; the
# processor keeps running. We only re-baseline the on-screen display history.
self._history_run_signature = None
if self._supervisor.is_processor_running():
self._stop_all_processes()
self._reset_runtime_history()
# Tell the still-running data_processor to drop its accumulated background/
# history so the new reference takes effect cleanly (no old/new-reference
# blend) without restarting the process. Reuses the live-config clear_all
# command, applied on the processor's next poll.
if self._supervisor.is_processor_running():
self._write_live_processing_config(history_command="clear_all", bump_history_seq=True)
self._refresh_preprocess_summary_labels()
if collection.traces:
self._draw_single_trace(