#!/usr/bin/env bash set -Eeuo pipefail timestamp() { date '+%Y-%m-%d %H:%M:%S' } log() { printf '[%s] %s\n' "$(timestamp)" "$*" } fail() { log "ERROR: $*" exit 1 } report_err() { local exit_code=$? local line_no=${1:-unknown} local cmd=${2:-unknown} log "ERROR: command failed at line ${line_no}: ${cmd} (exit=${exit_code})" exit "$exit_code" } trap 'report_err "${LINENO}" "${BASH_COMMAND}"' ERR SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" DEFAULT_APP_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)" RUN_USER="${RUN_USER:-$(id -un)}" APP_DIR="${APP_DIR:-${DEFAULT_APP_DIR}}" LOG_DIR="${LOG_DIR:-${APP_DIR}/logs}" HOTSPOT_SSID="${HOTSPOT_SSID:-}" HOTSPOT_PASSWORD="${HOTSPOT_PASSWORD:-}" SERVER_IP="${SERVER_IP:-10.0.0.5}" SERVER_PORT="${SERVER_PORT:-9000}" ROS_SETUP="${ROS_SETUP:-/home/${RUN_USER}/ros2ws/install/setup.bash}" PYTHON_BIN="${PYTHON_BIN:-python3}" WIFI_RETRY_INTERVAL="${WIFI_RETRY_INTERVAL:-5}" PING_RETRY_INTERVAL="${PING_RETRY_INTERVAL:-5}" VENV_ACTIVATE="${APP_DIR}/.venv/bin/activate" LOG_RETENTION_COUNT="${LOG_RETENTION_COUNT:-20}" mkdir -p "${LOG_DIR}" LOG_FILE="${LOG_DIR}/monitor_sender_$(date '+%Y%m%d_%H%M%S').log" touch "${LOG_FILE}" exec > >(tee -a "${LOG_FILE}") 2>&1 require_command() { local command_name=$1 command -v "${command_name}" >/dev/null 2>&1 || fail "missing required command: ${command_name}" } prune_old_logs() { local prune_from local old_logs=() [[ "${LOG_RETENTION_COUNT}" =~ ^[0-9]+$ ]] || fail "LOG_RETENTION_COUNT must be a positive integer" (( LOG_RETENTION_COUNT >= 1 )) || fail "LOG_RETENTION_COUNT must be at least 1" prune_from=$((LOG_RETENTION_COUNT + 1)) mapfile -t old_logs < <(ls -1dt "${LOG_DIR}"/monitor_sender_*.log 2>/dev/null | tail -n +"${prune_from}") if ((${#old_logs[@]} > 0)); then rm -f -- "${old_logs[@]}" log "Pruned ${#old_logs[@]} old log file(s); keeping latest ${LOG_RETENTION_COUNT}" fi } source_with_relaxed_mode() { local target_file=$1 local shell_flags=$- local err_trap local source_status=0 err_trap="$(trap -p ERR || true)" trap - ERR set +eu # shellcheck disable=SC1090,SC1091 source "${target_file}" source_status=$? [[ "${shell_flags}" == *e* ]] && set -e || set +e [[ "${shell_flags}" == *u* ]] && set -u || set +u if [[ -n "${err_trap}" ]]; then eval "${err_trap}" fi return "${source_status}" } get_wifi_device() { nmcli -t -f DEVICE,TYPE device status | awk -F: '$2 == "wifi" { print $1; exit }' } get_active_connection_name() { local wifi_device=$1 nmcli -g GENERAL.CONNECTION device show "${wifi_device}" 2>/dev/null | head -n1 | tr -d '\r' } get_active_ssid() { local wifi_device=$1 local active_connection active_connection="$(get_active_connection_name "${wifi_device}" || true)" if [[ -z "${active_connection}" || "${active_connection}" == "--" ]]; then return 1 fi nmcli -g 802-11-wireless.ssid connection show "${active_connection}" 2>/dev/null | head -n1 | tr -d '\r' } is_hotspot_connected() { local wifi_device=$1 local active_ssid active_ssid="$(get_active_ssid "${wifi_device}" || true)" [[ -n "${active_ssid}" && "${active_ssid}" == "${HOTSPOT_SSID}" ]] } connect_hotspot() { local wifi_device=$1 while true; do if is_hotspot_connected "${wifi_device}"; then log "Wi-Fi already connected to hotspot '${HOTSPOT_SSID}'" return 0 fi log "Trying hotspot '${HOTSPOT_SSID}' on Wi-Fi device '${wifi_device}'" nmcli device wifi rescan ifname "${wifi_device}" >/dev/null 2>&1 || true if nmcli connection up "${HOTSPOT_SSID}" ifname "${wifi_device}" >/dev/null 2>&1; then sleep 2 if is_hotspot_connected "${wifi_device}"; then log "Connected to hotspot '${HOTSPOT_SSID}' via saved NetworkManager profile" return 0 fi fi if [[ -n "${HOTSPOT_PASSWORD}" ]]; then if nmcli device wifi connect "${HOTSPOT_SSID}" password "${HOTSPOT_PASSWORD}" ifname "${wifi_device}" >/dev/null 2>&1; then sleep 2 if is_hotspot_connected "${wifi_device}"; then log "Connected to hotspot '${HOTSPOT_SSID}' via direct nmcli connect" return 0 fi fi else log "HOTSPOT_PASSWORD is empty; skipping direct nmcli connect and relying on saved profile" fi log "Hotspot '${HOTSPOT_SSID}' is not ready yet; retrying in ${WIFI_RETRY_INTERVAL}s" sleep "${WIFI_RETRY_INTERVAL}" done } wait_for_server() { while true; do if ping -c 1 -W 2 "${SERVER_IP}" >/dev/null 2>&1; then log "Server '${SERVER_IP}' is reachable" return 0 fi log "Server '${SERVER_IP}' is not reachable yet; retrying in ${PING_RETRY_INTERVAL}s" sleep "${PING_RETRY_INTERVAL}" done } log "Starting xMonitor sender bootstrap" log "Using APP_DIR='${APP_DIR}'" log "Log file: ${LOG_FILE}" prune_old_logs [[ -n "${HOTSPOT_SSID}" ]] || fail "HOTSPOT_SSID must be set in /etc/xmonitor/xmonitor.env" [[ -d "${APP_DIR}" ]] || fail "APP_DIR does not exist: ${APP_DIR}" [[ -f "${APP_DIR}/monitor_sender.py" ]] || fail "monitor_sender.py not found in ${APP_DIR}" [[ -f "${ROS_SETUP}" ]] || fail "ROS setup file not found: ${ROS_SETUP}" [[ -f "${VENV_ACTIVATE}" ]] || fail "Python virtualenv activate script not found: ${VENV_ACTIVATE}" require_command nmcli require_command ping require_command tee require_command "${PYTHON_BIN}" if [[ "$(id -un)" != "${RUN_USER}" ]]; then log "Warning: script is running as '$(id -un)' but RUN_USER is '${RUN_USER}'" fi WIFI_DEVICE="$(get_wifi_device)" [[ -n "${WIFI_DEVICE}" ]] || fail "No Wi-Fi device was found by NetworkManager" log "Detected Wi-Fi device '${WIFI_DEVICE}'" connect_hotspot "${WIFI_DEVICE}" wait_for_server log "Sourcing ROS environment '${ROS_SETUP}'" source_with_relaxed_mode "${ROS_SETUP}" log "Activating Python virtualenv '${VENV_ACTIVATE}'" source_with_relaxed_mode "${VENV_ACTIVATE}" cd "${APP_DIR}" log "Launching monitor_sender.py --ip ${SERVER_IP} --port ${SERVER_PORT}" exec "${PYTHON_BIN}" monitor_sender.py --ip "${SERVER_IP}" --port "${SERVER_PORT}"