The sar3d processor is rewritten as a checked port of Multi_antenna_3D_clean_v1.ipynb: NVRTC kernels, timing from the trace stamps, notebook parameter names and one cubic SAR_VOXEL_M. It adds front, side, top and 3D views, drops gpr_cpp and the parity tools, and the GUI speed now applies only while the socket speed is ignored.
507 lines
18 KiB
Bash
Executable File
507 lines
18 KiB
Bash
Executable File
#!/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=""
|
|
|
|
# Single-instance coordination: the headless daemon and the interactive GUI
|
|
# must never run at once (they share the radar, SHM rings and locator port).
|
|
SERVICE_NAME="radar.service"
|
|
LOCK_FILE="/tmp/radar_system.lock"
|
|
|
|
SKIP_BUILD=0
|
|
BUILD_ONLY=0
|
|
CLEAN_SHM=0
|
|
AUTO_START=0
|
|
PRODUCER_ONLY=0
|
|
HEADLESS=0
|
|
# Acquisition device, detected from the active run config's radar.model. Drives
|
|
# device-specific provisioning, dependencies, and which collector binary to build
|
|
# — so every device launches the same way (no per-device flags).
|
|
RADAR_MODEL=""
|
|
|
|
print_usage() {
|
|
cat <<'EOF'
|
|
Usage: ./start.sh [options]
|
|
|
|
Options:
|
|
--profile PATH Use a specific GUI/run config profile (device is auto-detected
|
|
from its radar.model)
|
|
--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
|
|
;;
|
|
--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 [[ -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
|
|
}
|
|
|
|
# Resolve the config that will actually be used (explicit --profile, else the
|
|
# active run_config.json) and read its radar.model. Best-effort: any failure
|
|
# falls back to 'librevna' (the full-provisioning superset), so detection can
|
|
# never make a launch less safe. Uses system python3 (the venv may not exist yet).
|
|
detect_radar_model() {
|
|
local config_path="${PROFILE_PATH:-${PROJECT_ROOT}/run_config.json}"
|
|
RADAR_MODEL="librevna"
|
|
[[ -f "${config_path}" ]] || return
|
|
local detected
|
|
detected="$(python3 -c '
|
|
import json, sys
|
|
try:
|
|
with open(sys.argv[1]) as handle:
|
|
data = json.load(handle)
|
|
radar = data.get("radar") if isinstance(data, dict) else {}
|
|
model = radar.get("model") if isinstance(radar, dict) else None
|
|
print(model or "librevna")
|
|
except Exception:
|
|
print("librevna")
|
|
' "${config_path}" 2>/dev/null)" || detected=""
|
|
[[ -n "${detected}" ]] && RADAR_MODEL="${detected}"
|
|
echo "[start.sh] Detected radar model: ${RADAR_MODEL} (config: ${config_path})"
|
|
}
|
|
|
|
# radar.replay.dataset_dir of a config, empty unless it replays a recording.
|
|
replay_dataset_dir() {
|
|
python3 -c '
|
|
import json, sys
|
|
try:
|
|
radar = json.load(open(sys.argv[1])).get("radar", {})
|
|
print(str(radar.get("replay", {}).get("dataset_dir", "")).strip())
|
|
except Exception:
|
|
print("")
|
|
' "$1" 2>/dev/null
|
|
}
|
|
|
|
# Acquisition producer for the active model, mirroring the process supervisor's
|
|
# selection so --producer-only behaves identically to a full pipeline launch: a
|
|
# replay dataset takes precedence over the model's producer.
|
|
producer_command() {
|
|
local config_path="$1"
|
|
if [[ -n "$(replay_dataset_dir "${config_path}")" ]]; then
|
|
printf '%s\0' "${PYTHON_CMD}" -m python_app.scripts.replay_raw_producer --config "${config_path}"
|
|
return
|
|
fi
|
|
case "${RADAR_MODEL}" in
|
|
kamil_adc)
|
|
printf '%s\0' "${PYTHON_CMD}" -m python_app.scripts.kamil_adc_raw_producer --config "${config_path}" ;;
|
|
librevna_multi|sn9000)
|
|
printf '%s\0' "${PYTHON_CMD}" -m python_app.scripts.matrix_raw_producer --config "${config_path}" ;;
|
|
*)
|
|
printf '%s\0' "${PROJECT_ROOT}/build/bin/sweep_orchestrator" --config "${config_path}" ;;
|
|
esac
|
|
}
|
|
|
|
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 [[ "${RADAR_MODEL}" == "kamil_adc" ]]; then
|
|
# Kamil ADC has no VISA dependency (it talks to its own L-Card collector).
|
|
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
|
|
|
|
if ((HEADLESS == 1)); then
|
|
# Headless = unattended (often offline) appliance: never attempt a network
|
|
# pip install that could hang or crash-loop the service. Provisioning is a
|
|
# one-time interactive step. Fail fast with a clear, actionable message.
|
|
echo "Python dependencies are missing and headless mode does not provision them." >&2
|
|
echo "Run an interactive './start.sh' once (online) to create the venv, then retry." >&2
|
|
exit 1
|
|
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() {
|
|
# Qt >= 6.5 xcb platform plugin needs libxcb-cursor0; aarch64/Jetson images
|
|
# (and some minimal desktops) ship without it, and the GUI then dies at
|
|
# startup with "could not load the Qt platform plugin xcb". apt-only: other
|
|
# distros/managers name these differently and usually have them preinstalled.
|
|
local qt_gui_packages_missing=0
|
|
if command -v apt-get >/dev/null 2>&1 && command -v dpkg-query >/dev/null 2>&1; then
|
|
local package
|
|
for package in libxcb-cursor0 libxkbcommon-x11-0; do
|
|
if ! dpkg-query -W -f='${Status}' "${package}" 2>/dev/null | grep -q "install ok installed"; then
|
|
qt_gui_packages_missing=1
|
|
fi
|
|
done
|
|
fi
|
|
|
|
if command -v pkg-config >/dev/null 2>&1 && pkg-config --exists libusb-1.0 \
|
|
&& ((qt_gui_packages_missing == 0)); then
|
|
return
|
|
fi
|
|
|
|
echo "[start.sh] Installing system dependencies: libusb-1.0 dev headers (and Qt xcb libraries on apt)..."
|
|
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 libxcb-cursor0 libxkbcommon-x11-0
|
|
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() {
|
|
local jobs
|
|
jobs="${BUILD_JOBS:-$(nproc)}"
|
|
# The Kamil ADC collector is an extra, device-specific binary built only for
|
|
# that model; all models share the core pipeline binaries (`all`).
|
|
local targets="all"
|
|
if [[ "${RADAR_MODEL}" == "kamil_adc" ]]; then
|
|
targets="all kamil_adc_collector"
|
|
fi
|
|
if make -C "${PROJECT_ROOT}" -q ${targets} >/dev/null 2>&1; then
|
|
echo "[start.sh] C++ binaries are up to date; skipping build."
|
|
return
|
|
fi
|
|
echo "[start.sh] Building C++ binaries (jobs=${jobs}, targets: ${targets})..."
|
|
make -C "${PROJECT_ROOT}" -j"${jobs}" ${targets}
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
kill_stale_adc_collector() {
|
|
# The L-Card ADC collector runs in its own session and can outlive a crashed
|
|
# or force-killed run, holding the E-502 device and hanging the next start.
|
|
# Kill any leftover hard before launching so acquisition always opens cleanly.
|
|
if command -v pkill >/dev/null 2>&1; then
|
|
if pkill -9 -f kamil_adc_collector 2>/dev/null; then
|
|
echo "[start.sh] Killed leftover ADC collector process(es)."
|
|
fi
|
|
fi
|
|
}
|
|
|
|
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() {
|
|
local config_path="${PROFILE_PATH:-${PROJECT_ROOT}/run_config.json}"
|
|
if [[ ! -f "${config_path}" ]]; then
|
|
echo "--producer-only needs a config (pass --profile, or create run_config.json)." >&2
|
|
exit 1
|
|
fi
|
|
|
|
export PYTHONPATH="${PROJECT_ROOT}${PYTHONPATH:+:${PYTHONPATH}}"
|
|
# Run the acquisition producer the supervisor would pick for this model.
|
|
local -a command=()
|
|
while IFS= read -r -d '' token; do
|
|
command+=("${token}")
|
|
done < <(producer_command "${config_path}")
|
|
echo "[start.sh] Starting ${RADAR_MODEL} producer: ${command[*]}"
|
|
exec "${command[@]}"
|
|
}
|
|
|
|
stop_headless_service() {
|
|
# Stop the headless daemon (if running) so it releases the radar, SHM rings
|
|
# and locator port before the interactive GUI/producer takes over. Relies on
|
|
# the passwordless sudoers rule installed by deploy/install-daemon.sh. Safe
|
|
# no-op when systemd or the unit is absent (is-active is false -> skip).
|
|
command -v systemctl >/dev/null 2>&1 || return 0
|
|
if systemctl is-active --quiet "${SERVICE_NAME}"; then
|
|
echo "[start.sh] Stopping ${SERVICE_NAME} so the GUI can take over the hardware..."
|
|
# Non-fatal: a sudo/systemctl failure must not abort the GUI launch under
|
|
# `set -e`. The single-instance lock below still prevents a real conflict.
|
|
if ! sudo systemctl stop "${SERVICE_NAME}"; then
|
|
echo "[start.sh] WARNING: failed to stop ${SERVICE_NAME}; relying on the instance lock." >&2
|
|
fi
|
|
fi
|
|
}
|
|
|
|
verify_cpp_binaries() {
|
|
# Guard the daemon's --skip-build path: refuse to run against a tree whose C++
|
|
# binaries were never built, instead of failing obscurely at spawn time.
|
|
local missing=0 bin
|
|
local required_bins="data_processor data_preprocessor"
|
|
# Kamil ADC also needs its acquisition collector; other models use the C++
|
|
# sweep_orchestrator, which `all` always builds.
|
|
if [[ "${RADAR_MODEL}" == "kamil_adc" ]]; then
|
|
required_bins="${required_bins} kamil_adc_collector"
|
|
fi
|
|
for bin in ${required_bins}; do
|
|
if [[ ! -x "${PROJECT_ROOT}/build/bin/${bin}" ]]; then
|
|
echo "Required binary missing or not executable: build/bin/${bin}" >&2
|
|
missing=1
|
|
fi
|
|
done
|
|
if ((missing == 1)); then
|
|
echo "Build the C++ binaries first: run ./start.sh without --skip-build, or 'make all'." >&2
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
acquire_single_instance_lock() {
|
|
# Fail fast if another instance already owns the hardware, instead of
|
|
# surfacing a cryptic 'shm ring busy' / 'port in use' later. fd 9 survives
|
|
# the exec into Python, so the lock is held for the whole app lifetime.
|
|
exec 9>"${LOCK_FILE}"
|
|
if ! flock -n 9; then
|
|
echo "[start.sh] Another radar instance already holds ${LOCK_FILE}; aborting." >&2
|
|
echo "[start.sh] Stop it first: sudo systemctl stop ${SERVICE_NAME} (or close the other run)." >&2
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
main() {
|
|
parse_args "$@"
|
|
resolve_profile_path
|
|
check_environment
|
|
detect_radar_model
|
|
# Skip first-time provisioning (build headers, USB udev rule) in headless
|
|
# mode: the daemon runs unattended at boot as a non-root user and must not
|
|
# block on sudo. A fresh machine is provisioned by one interactive launch.
|
|
# Also skip it for non-LibreVNA devices (e.g. Kamil ADC), which neither use
|
|
# libusb directly nor need the LibreVNA USB access rule.
|
|
if [[ "${RADAR_MODEL}" != "kamil_adc" ]] && ((HEADLESS == 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
|
|
|
|
verify_cpp_binaries
|
|
|
|
if ((BUILD_ONLY == 1)); then
|
|
echo "[start.sh] Build completed."
|
|
exit 0
|
|
fi
|
|
|
|
# Interactive GUI/producer launch: stop the headless daemon first so it
|
|
# frees the radar, SHM and locator port. The daemon itself runs --headless
|
|
# and skips this, so it never stops itself.
|
|
if ((HEADLESS == 0)); then
|
|
stop_headless_service
|
|
fi
|
|
acquire_single_instance_lock
|
|
# Clear any ADC collector wedged by a previous run before we launch a new one.
|
|
kill_stale_adc_collector
|
|
|
|
if ((PRODUCER_ONLY == 1)); then
|
|
run_producer_only
|
|
fi
|
|
|
|
run_gui
|
|
}
|
|
|
|
main "$@"
|