#!/usr/bin/env bash set -euo pipefail SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" PROJECT_ROOT="${SCRIPT_DIR}" VENV_PYTHON="${PROJECT_ROOT}/.venv/bin/python" VENV_PIP="${PROJECT_ROOT}/.venv/bin/pip" GUI_ENTRY="${PROJECT_ROOT}/python_app/gui/main.py" REQUIREMENTS_FILE="${PROJECT_ROOT}/requirements.txt" PYTHON_CMD="" PROFILE_PATH="" SKIP_BUILD=0 BUILD_ONLY=0 CLEAN_SHM=0 KAMIL_ADC_MODE=0 AUTO_START=0 PRODUCER_ONLY=0 HEADLESS=0 print_usage() { cat <<'EOF' Usage: ./start.sh [options] Options: --kamil-adc Use the Raspberry Pi Kamil ADC profile --profile PATH Use a specific GUI/run config profile --auto-start Start the GUI pipeline automatically after launch --headless Run without a display (Qt offscreen platform) and apply the active radar config, then start the pipeline. Suitable for unattended Raspberry Pi deployments. Implies --auto-start. --producer-only Run only the raw producer selected by the profile --skip-build Skip C++ build step --build-only Build C++ binaries and exit --clean-shm Remove known radar shared-memory segments before start -h, --help Show this help EOF } parse_args() { while (($# > 0)); do case "$1" in --skip-build) SKIP_BUILD=1 ;; --build-only) BUILD_ONLY=1 ;; --clean-shm) CLEAN_SHM=1 ;; --kamil-adc) KAMIL_ADC_MODE=1 ;; --profile) if (($# < 2)); then echo "--profile requires a path argument." >&2 exit 1 fi PROFILE_PATH="$2" shift ;; --auto-start) AUTO_START=1 ;; --headless) HEADLESS=1 AUTO_START=1 ;; --producer-only) PRODUCER_ONLY=1 ;; -h|--help) print_usage exit 0 ;; *) echo "Unknown argument: $1" >&2 print_usage exit 1 ;; esac shift done } absolute_path() { local path="$1" if [[ "${path}" = /* ]]; then printf '%s\n' "${path}" return fi printf '%s\n' "${PROJECT_ROOT}/${path}" } resolve_profile_path() { if ((KAMIL_ADC_MODE == 1)); then if [[ -z "${PROFILE_PATH}" ]]; then if [[ -f "${PROJECT_ROOT}/run_config_kamil_adc.pi.json" ]]; then PROFILE_PATH="${PROJECT_ROOT}/run_config_kamil_adc.pi.json" else PROFILE_PATH="${PROJECT_ROOT}/run_config_kamil_adc.example.json" fi fi fi if [[ -n "${PROFILE_PATH}" ]]; then PROFILE_PATH="$(absolute_path "${PROFILE_PATH}")" if [[ ! -f "${PROFILE_PATH}" ]]; then echo "Config profile not found: ${PROFILE_PATH}" >&2 exit 1 fi fi } check_environment() { if ! command -v python3 >/dev/null 2>&1; then echo "python3 is not installed or not found in PATH." >&2 exit 1 fi if [[ ! -f "${REQUIREMENTS_FILE}" ]]; then echo "Requirements file not found: ${REQUIREMENTS_FILE}" >&2 exit 1 fi if [[ ! -f "${GUI_ENTRY}" ]]; then echo "GUI entry not found: ${GUI_ENTRY}" >&2 exit 1 fi } ensure_python_dependencies() { local dependency_check if ((KAMIL_ADC_MODE == 1)); then dependency_check='import numpy, serial, PyQt6, pyqtgraph, usb1' else dependency_check='import numpy, serial, PyQt6, pyqtgraph, usb1, pyvisa' fi if [[ ! -x "${VENV_PYTHON}" ]]; then echo "[start.sh] Creating virtual environment..." python3 -m venv "${PROJECT_ROOT}/.venv" fi if [[ ! -x "${VENV_PIP}" ]]; then echo "pip is missing in virtual environment: ${VENV_PIP}" >&2 exit 1 fi if "${VENV_PYTHON}" -c "${dependency_check}" >/dev/null 2>&1; then PYTHON_CMD="${VENV_PYTHON}" return fi echo "[start.sh] Installing Python dependencies into virtual environment..." "${VENV_PIP}" install --upgrade pip "${VENV_PIP}" install -r "${REQUIREMENTS_FILE}" if ! "${VENV_PYTHON}" -c "${dependency_check}" >/dev/null 2>&1; then echo "Required Python dependencies are still unavailable in virtual environment: ${PROJECT_ROOT}/.venv" >&2 exit 1 fi PYTHON_CMD="${VENV_PYTHON}" echo "[start.sh] Using virtual environment: ${PYTHON_CMD}" } run_privileged() { if [[ "${EUID}" -eq 0 ]]; then "$@" return fi if command -v sudo >/dev/null 2>&1; then sudo "$@" return fi echo "Need elevated privileges to run: $*" >&2 echo "Run as root or install 'sudo'." >&2 exit 1 } ensure_system_dependencies() { if command -v pkg-config >/dev/null 2>&1 && pkg-config --exists libusb-1.0; then return fi echo "[start.sh] Installing system dependency: libusb-1.0 dev headers..." if command -v apt-get >/dev/null 2>&1; then run_privileged apt-get update run_privileged apt-get install -y pkg-config libusb-1.0-0-dev elif command -v dnf >/dev/null 2>&1; then run_privileged dnf install -y pkgconf-pkg-config libusb1-devel elif command -v yum >/dev/null 2>&1; then run_privileged yum install -y pkgconfig libusb1-devel elif command -v pacman >/dev/null 2>&1; then run_privileged pacman -Sy --needed pkgconf libusb elif command -v zypper >/dev/null 2>&1; then run_privileged zypper --non-interactive install pkg-config libusb-1_0-devel elif command -v brew >/dev/null 2>&1; then brew install pkg-config libusb else echo "Could not detect supported package manager." >&2 echo "Install manually: pkg-config and libusb development package." >&2 exit 1 fi if ! command -v pkg-config >/dev/null 2>&1 || ! pkg-config --exists libusb-1.0; then echo "libusb-1.0 development package is still unavailable after install attempt." >&2 exit 1 fi } ensure_usb_access_rules() { if [[ "$(uname -s)" != "Linux" ]]; then return fi if ! command -v udevadm >/dev/null 2>&1; then return fi local rule_file="/etc/udev/rules.d/99-radar-librevna.rules" local tmp_rule tmp_rule="$(mktemp)" cat > "${tmp_rule}" <<'EOF' # LibreVNA USB access for non-root users SUBSYSTEM=="usb", ATTR{idVendor}=="0483", ATTR{idProduct}=="564e", GROUP="plugdev", MODE="0660", TAG+="uaccess" SUBSYSTEM=="usb", ATTR{idVendor}=="0483", ATTR{idProduct}=="4121", GROUP="plugdev", MODE="0660", TAG+="uaccess" SUBSYSTEM=="usb", ATTR{idVendor}=="1209", ATTR{idProduct}=="4121", GROUP="plugdev", MODE="0660", TAG+="uaccess" EOF if [[ -f "${rule_file}" ]] && cmp -s "${tmp_rule}" "${rule_file}"; then rm -f "${tmp_rule}" return fi echo "[start.sh] Installing udev rule for LibreVNA USB access..." run_privileged install -m 0644 "${tmp_rule}" "${rule_file}" rm -f "${tmp_rule}" run_privileged udevadm control --reload-rules run_privileged udevadm trigger echo "[start.sh] udev rules updated. Reconnect USB device if it is already plugged in." } build_cpp_binaries() { if make -C "${PROJECT_ROOT}" -q all >/dev/null 2>&1; then echo "[start.sh] C++ binaries are up to date; skipping build." return fi local jobs jobs="${BUILD_JOBS:-$(nproc)}" echo "[start.sh] Building C++ binaries (jobs=${jobs})..." make -C "${PROJECT_ROOT}" -j"${jobs}" all } cleanup_known_shm() { echo "[start.sh] Cleaning known shared-memory segments..." rm -f \ /dev/shm/radar_raw \ /dev/shm/radar_raw_tap \ /dev/shm/radar_preprocessed \ /dev/shm/radar_preprocessed_tap \ /dev/shm/radar_results \ /dev/shm/radar_raw_kamil_adc \ /dev/shm/radar_raw_tap_kamil_adc \ /dev/shm/radar_preprocessed_kamil_adc \ /dev/shm/radar_preprocessed_tap_kamil_adc \ /dev/shm/radar_results_kamil_adc \ || true } run_gui() { if [[ -n "${PROFILE_PATH}" ]]; then export RADAR_SYSTEM_PROFILE="${PROFILE_PATH}" echo "[start.sh] Using config profile: ${PROFILE_PATH}" fi if ((AUTO_START == 1)); then export RADAR_SYSTEM_AUTO_START=1 echo "[start.sh] GUI auto-start is enabled." fi if ((HEADLESS == 1)); then # Qt offscreen platform lets the GUI controller and its event loop run # on a machine with no display (typical Raspberry Pi deployment). All # backend services — supervisor, Python device drivers, SHM readers, # locator client (vlc) handling — continue to work unchanged. export QT_QPA_PLATFORM=offscreen export RADAR_SYSTEM_HEADLESS=1 export RADAR_SYSTEM_AUTO_APPLY_RADAR=1 echo "[start.sh] Headless mode: Qt offscreen + auto apply-radar + auto-start." fi echo "[start.sh] Launching GUI..." exec "${PYTHON_CMD}" "${GUI_ENTRY}" } run_producer_only() { if ((KAMIL_ADC_MODE != 1)); then echo "--producer-only currently requires --kamil-adc." >&2 exit 1 fi if [[ -z "${PROFILE_PATH}" ]]; then echo "--producer-only requires a resolved config profile." >&2 exit 1 fi export PYTHONPATH="${PROJECT_ROOT}${PYTHONPATH:+:${PYTHONPATH}}" echo "[start.sh] Starting Kamil ADC raw producer with profile: ${PROFILE_PATH}" exec "${PYTHON_CMD}" -m python_app.scripts.kamil_adc_raw_producer --config "${PROFILE_PATH}" } main() { parse_args "$@" resolve_profile_path check_environment if ((KAMIL_ADC_MODE == 0)); then ensure_system_dependencies ensure_usb_access_rules fi ensure_python_dependencies if ((CLEAN_SHM == 1)); then cleanup_known_shm fi if ((SKIP_BUILD == 0)); then build_cpp_binaries fi if ((BUILD_ONLY == 1)); then echo "[start.sh] Build completed." exit 0 fi if ((PRODUCER_ONLY == 1)); then run_producer_only fi run_gui } main "$@"