diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..c11bfc0 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,8 @@ +{ + "permissions": { + "allow": [ + "Bash(find deploy:*)", + "Bash(ls -R deploy/ scripts/)" + ] + } +} diff --git a/.gitignore b/.gitignore index 5ae1032..aa59621 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,6 @@ env/ # macOS .DS_Store +# Runtime logs +logs/ + diff --git a/Plan.md b/Plan.md new file mode 100644 index 0000000..3130a57 --- /dev/null +++ b/Plan.md @@ -0,0 +1,162 @@ +自启方案:monitor_sender.py 通过手机热点自动启动 + + Context + + monitor_sender.py 是一个 ROS2 节点,运行在机器人控制器上(Ubuntu, 用户名 + ubuntu),订阅机器人状态话题并通过 WebSocket 发送到 PC + 端监控服务器。设备在真实世界运行,需要开机后: + 1. 自动连接手机热点 WiFi + 2. 等待网络就绪后启动 monitor_sender.py(通过热点向 PC 的固定 IP 发送数据) + 3. 崩溃后自动重启并记录崩溃原因 + + 方案:systemd 服务 + 启动脚本(两个文件搞定) + + 文件 1:启动脚本 ~/xMonitor/start_monitor.sh + + #!/bin/bash + # ========== 配置区(按实际修改) ========== + HOTSPOT_SSID="你的热点名称" # ← 稍后填 + HOTSPOT_PASS="你的热点密码" # ← 稍后填 + SERVER_IP="192.168.43.xxx" # ← 电脑在热点中的固定 IP + SERVER_PORT=9000 + # ========================================== + + LOG_DIR="$HOME/xMonitor/logs" + mkdir -p "$LOG_DIR" + LOG_FILE="$LOG_DIR/monitor_$(date +%Y%m%d_%H%M%S).log" + + exec > >(tee -a "$LOG_FILE") 2>&1 + echo "===== $(date) 启动 monitor_sender =====" + + # --- 1) 连接手机热点 --- + echo "[NET] 正在连接热点: $HOTSPOT_SSID" + MAX_WIFI_RETRY=30 + for i in $(seq 1 $MAX_WIFI_RETRY); do + # 尝试连接(如果已保存过连接配置则直接 up,否则新建) + nmcli dev wifi connect "$HOTSPOT_SSID" password "$HOTSPOT_PASS" 2>/dev/null \ + || nmcli con up "$HOTSPOT_SSID" 2>/dev/null + + # 检查是否拿到 IP + if nmcli -t -f GENERAL.STATE dev show wlan0 2>/dev/null | grep -q "100"; then + echo "[NET] WiFi 已连接 ($i/$MAX_WIFI_RETRY)" + break + fi + echo "[NET] WiFi 未就绪,重试 ($i/$MAX_WIFI_RETRY)..." + sleep 3 + done + + # --- 2) 等待能 ping 通服务器 --- + echo "[NET] 等待服务器 $SERVER_IP 可达..." + MAX_PING_RETRY=20 + for i in $(seq 1 $MAX_PING_RETRY); do + if ping -c 1 -W 2 "$SERVER_IP" >/dev/null 2>&1; then + echo "[NET] 服务器可达 ✓" + break + fi + echo "[NET] ping 失败,重试 ($i/$MAX_PING_RETRY)..." + sleep 3 + done + + # --- 3) Source 环境并启动 --- + echo "[ENV] source ROS2 workspace..." + source /home/ubuntu/ros2ws/install/setup.bash + + echo "[ENV] source Python venv..." + source "$HOME/xMonitor/.venv/bin/activate" + + echo "[RUN] 启动 monitor_sender.py --ip $SERVER_IP --port $SERVER_PORT" + cd "$HOME/xMonitor" + python3 monitor_sender.py --ip "$SERVER_IP" --port "$SERVER_PORT" + + EXIT_CODE=$? + echo "===== $(date) 进程退出, code=$EXIT_CODE =====" + exit $EXIT_CODE + + 文件 2:systemd 服务 /etc/systemd/system/xmonitor.service + + [Unit] + Description=xMonitor Robot Status Sender + After=network-manager.service + Wants=network-manager.service + + [Service] + Type=simple + User=ubuntu + WorkingDirectory=/home/ubuntu/xMonitor + ExecStart=/bin/bash /home/ubuntu/xMonitor/start_monitor.sh + + # --- 崩溃自动重启 --- + Restart=on-failure + RestartSec=10 + + # --- 崩溃日志 --- + StandardOutput=journal + StandardError=journal + SyslogIdentifier=xmonitor + + [Install] + WantedBy=multi-user.target + + 部署步骤(在机器人设备上执行) + + # 1. 将 start_monitor.sh 放到 ~/xMonitor/ 并加执行权限 + chmod +x ~/xMonitor/start_monitor.sh + + # 2. 编辑 start_monitor.sh 顶部配置区,填入热点名称、密码、电脑IP + + # 3. 复制 service 文件并启用 + sudo cp xmonitor.service /etc/systemd/system/ + sudo systemctl daemon-reload + sudo systemctl enable xmonitor.service + + # 4. 立即测试启动(可选) + sudo systemctl start xmonitor.service + + # 5. 查看状态 / 日志 + systemctl status xmonitor.service + journalctl -u xmonitor -f # 实时查看日志 + ls ~/xMonitor/logs/ # 查看历史日志文件 + + 关键设计说明 + + ┌────────────┬───────────────────────────────────────────────────────────────┐ + │ 需求 │ 实现方式 │ + ├────────────┼───────────────────────────────────────────────────────────────┤ + │ 连热点 │ nmcli dev wifi connect 循环重试,最多 30 次 × 3 秒 │ + ├────────────┼───────────────────────────────────────────────────────────────┤ + │ 等待网络 │ ping 服务器 IP 直到可达 │ + ├────────────┼───────────────────────────────────────────────────────────────┤ + │ source │ 脚本内 source .venv/bin/activate │ + │ 虚拟环境 │ │ + ├────────────┼───────────────────────────────────────────────────────────────┤ + │ source │ 脚本内 source /home/ubuntu/ros2ws/install/setup.bash(来自 │ + │ ROS2 │ README) │ + ├────────────┼───────────────────────────────────────────────────────────────┤ + │ 崩溃重启 │ systemd Restart=on-failure + RestartSec=10(10 秒后重启) │ + ├────────────┼───────────────────────────────────────────────────────────────┤ + │ 崩溃日志 │ 双保险:① journalctl -u xmonitor 系统日志 ② ~/xMonitor/logs/ │ + │ │ 下按时间戳命名的文本日志 │ + ├────────────┼───────────────────────────────────────────────────────────────┤ + │ 开机自启 │ systemctl enable xmonitor.service │ + └────────────┴───────────────────────────────────────────────────────────────┘ + + 待用户提供 + + - 手机热点 SSID + - 手机热点密码 + - 电脑在热点中的固定 IP(192.168.43.???) + + 需要创建/修改的文件 + + 1. 新建 ~/xMonitor/start_monitor.sh — 启动脚本 + 2. 新建 ~/xMonitor/xmonitor.service — systemd 服务文件(用户手动 sudo cp 到 + /etc/systemd/system/) + + 验证方法 + + 1. 手动运行 bash ~/xMonitor/start_monitor.sh 确认 WiFi 连接和程序启动正常 + 2. sudo systemctl start xmonitor.service 启动服务 + 3. systemctl status xmonitor.service 确认 active (running) + 4. journalctl -u xmonitor -f 实时查看日志 + 5. 杀掉 python 进程 kill $(pgrep -f monitor_sender) 验证自动重启 + 6. 重启设备验证开机自启 \ No newline at end of file diff --git a/README.md b/README.md index 3df6b4f..2e9ce4a 100644 --- a/README.md +++ b/README.md @@ -198,4 +198,75 @@ python3 monitor_sender.py --ip 10.0.0.5 --port 9000 | `main_monitor.py` | FastAPI server — hosts the web GUI and relays data to browsers | | `monitor_sender.py` | ROS 2 node — collects robot data and streams it to the server | | `README.md` | This file | +| `scripts/start_monitor_sender.sh` | Bootstrap script for hotspot connect, environment setup, and sender launch | +| `deploy/xmonitor.env.example` | Template for `/etc/xmonitor/xmonitor.env` | +| `deploy/xmonitor-sender.service` | `systemd` service template for robot-side autostart | + +--- + +## Robot Sender Autostart (Ubuntu + systemd) + +For field deployment on the robot controller, this repo now includes: + +- `scripts/start_monitor_sender.sh` +- `deploy/xmonitor.env.example` +- `deploy/xmonitor-sender.service` + +The design stays intentionally small: + +- the startup script keeps trying to join your phone hotspot with `nmcli` +- it waits until the PC hotspot IP is reachable +- it sources the ROS setup and `.venv` +- it launches `python3 monitor_sender.py --ip --port ` +- `systemd` restarts the process after crashes, while logs go to both `journalctl` and `~/xMonitor/logs/` + +### Install on the Ubuntu robot device + +Assuming the repo is deployed at `/home/ubuntu/xMonitor`: + +```bash +cd /home/ubuntu/xMonitor + +sudo install -d -m 700 /etc/xmonitor +sudo cp deploy/xmonitor.env.example /etc/xmonitor/xmonitor.env +sudo chmod 600 /etc/xmonitor/xmonitor.env +sudoedit /etc/xmonitor/xmonitor.env + +chmod +x scripts/start_monitor_sender.sh +sudo cp deploy/xmonitor-sender.service /etc/systemd/system/xmonitor-sender.service + +sudo systemctl daemon-reload +sudo systemctl enable --now xmonitor-sender.service +``` + +If you deploy the repo somewhere other than `/home/ubuntu/xMonitor`, update these together: + +- `User=` in `deploy/xmonitor-sender.service` +- `WorkingDirectory=` in `deploy/xmonitor-sender.service` +- `ExecStart=` in `deploy/xmonitor-sender.service` +- `APP_DIR=` and `RUN_USER=` in `/etc/xmonitor/xmonitor.env` + +### Runtime checks + +```bash +# Run the bootstrap script manually for troubleshooting +bash /home/ubuntu/xMonitor/scripts/start_monitor_sender.sh + +# Service state +systemctl status xmonitor-sender.service + +# Live logs from systemd +journalctl -u xmonitor-sender.service -f + +# Per-run log files +ls /home/ubuntu/xMonitor/logs +``` + +### Behavior + +- Hotspot connect retry: every 5 seconds until the target SSID is connected +- Server reachability retry: every 5 seconds until `SERVER_IP` answers `ping` +- Python process crash: `systemd` restarts after 5 seconds +- WebSocket drop after startup: handled by `monitor_sender.py`, which already retries every 3 seconds +- Per-run log files are pruned automatically; default retention is the latest 20 files via `LOG_RETENTION_COUNT` diff --git a/WINDOWS_RECEIVER_SETUP.md b/WINDOWS_RECEIVER_SETUP.md new file mode 100644 index 0000000..c69faf3 --- /dev/null +++ b/WINDOWS_RECEIVER_SETUP.md @@ -0,0 +1,273 @@ +# Windows 接收端环境配置 + +这份文档只针对接收方,也就是你的 Windows 电脑。 + +本文假设: + +- `xMonitor` 仓库就在 Windows 上 +- 你的电脑在手机热点网络里的固定 IP 是 `10.0.0.5` +- 发送端会连到 `10.0.0.5:9000` +- 你希望在 Windows 上运行 `main_monitor.py` 来接收机器人状态并在浏览器里显示 + +--- + +## 1. 安装 Python + +建议使用 Python 3.10 或更新版本。 + +在 PowerShell 里确认 Python 可用: + +```powershell +py -V +``` + +如果没有安装 Python: + +1. 打开 https://www.python.org/downloads/windows/ +2. 安装最新版 Python 3 +3. 安装时勾选 `Add Python to PATH` + +--- + +## 2. 进入项目目录 + +在 PowerShell 中进入仓库目录: + +```powershell +cd C:\Users\64187\Desktop\Workspace\xMonitor +``` + +--- + +## 3. 创建并启用虚拟环境 + +创建虚拟环境: + +```powershell +py -3 -m venv .venv +``` + +启用虚拟环境: + +```powershell +.\.venv\Scripts\Activate.ps1 +``` + +如果 PowerShell 阻止脚本执行,可先临时放开当前会话: + +```powershell +Set-ExecutionPolicy -Scope Process Bypass +.\.venv\Scripts\Activate.ps1 +``` + +启用后命令行前面通常会出现 `(.venv)`。 + +--- + +## 4. 安装接收端依赖 + +`main_monitor.py` 接收端只需要这两个包: + +- `fastapi` +- `uvicorn` + +安装命令: + +```powershell +python -m pip install --upgrade pip +pip install fastapi uvicorn +``` + +--- + +## 5. 启动接收端 + +因为你的发送端现在要往 `10.0.0.5:9000` 发,所以接收端必须监听 `9000` 端口。 + +在 PowerShell 中运行: + +```powershell +uvicorn main_monitor:app --host 0.0.0.0 --port 9000 +``` + +说明: + +- `--host 0.0.0.0`:允许热点网络里的设备访问这台 Windows 电脑 +- `--port 9000`:和发送端启动参数保持一致 + +如果你以后改了发送端端口,这里的端口也必须一起改。 + +--- + +## 6. 放行 Windows 防火墙 + +第一次启动时,Windows 可能会弹出防火墙提示。 + +请选择: + +- 允许访问 +- 至少勾选当前使用的网络类型 + +如果没有弹窗,可以手动放行 TCP 9000: + +```powershell +New-NetFirewallRule ` + -DisplayName "xMonitor 9000" ` + -Direction Inbound ` + -Protocol TCP ` + -LocalPort 9000 ` + -Action Allow +``` + +查看规则是否创建成功: + +```powershell +Get-NetFirewallRule -DisplayName "xMonitor 9000" +``` + +--- + +## 7. 检查本机热点 IP + +确认 Windows 当前热点网卡上的 IP 确实是你要给发送端使用的固定地址。 + +查看 IP: + +```powershell +ipconfig +``` + +确认对应热点网络适配器上有: + +```text +IPv4 Address . . . . . . . . . . : 10.0.0.5 +``` + +如果不是这个地址,要么把 Windows 侧固定 IP 配好,要么把发送端里的 `--ip` 改成实际地址。 + +--- + +## 8. 打开监控页面 + +接收端启动成功后,在本机浏览器打开: + +```text +http://127.0.0.1:9000 +``` + +或者直接打开: + +```text +http://10.0.0.5:9000 +``` + +如果发送端已经开始推数据,页面会显示机器人状态。 + +--- + +## 9. 验证接收端是否正常工作 + +### 看端口是否监听 + +```powershell +netstat -ano | findstr :9000 +``` + +能看到 `LISTENING` 说明服务已经起来了。 + +### 看浏览器页面是否能打开 + +打开: + +```text +http://127.0.0.1:9000 +``` + +若页面能打开,说明 HTTP 服务正常。 + +### 看是否收到机器人数据 + +接收端会把机器人上报的数据写到: + +```text +logs\robot_packets.jsonl +``` + +可以在项目目录下查看: + +```powershell +Get-Content .\logs\robot_packets.jsonl -Tail 5 +``` + +如果这里不断有新行,说明 WebSocket 接收正常。 + +--- + +## 10. 常见问题 + +### 1) 浏览器能打开页面,但没有机器人数据 + +通常检查这几项: + +- 发送端是否真的发到了 `10.0.0.5:9000` +- Windows 防火墙是否放行了 `9000` +- 电脑和设备是否确实连在同一个手机热点下 +- Windows 当前热点 IP 是否还是 `10.0.0.5` + +### 2) PowerShell 里 `Activate.ps1` 不能执行 + +先执行: + +```powershell +Set-ExecutionPolicy -Scope Process Bypass +``` + +然后再执行: + +```powershell +.\.venv\Scripts\Activate.ps1 +``` + +### 3) `uvicorn` 命令找不到 + +通常是虚拟环境没激活,或者依赖没装成功。 + +重新执行: + +```powershell +.\.venv\Scripts\Activate.ps1 +pip install fastapi uvicorn +``` + +### 4) 9000 端口被占用 + +查看占用: + +```powershell +netstat -ano | findstr :9000 +``` + +如果必须换端口,比如改成 `8000`,那就要同时改两边: + +- Windows 接收端启动端口 +- 发送端 `monitor_sender.py --port ...` + +--- + +## 11. 最小启动流程 + +以后你在 Windows 上最少只需要这几步: + +```powershell +cd C:\Users\64187\Desktop\Workspace\xMonitor +.\.venv\Scripts\Activate.ps1 +uvicorn main_monitor:app --host 0.0.0.0 --port 9000 +``` + +然后浏览器打开: + +```text +http://127.0.0.1:9000 +``` + +如果需要,我下一步可以再给你补一份“Windows 接收端开机自启”文档,直接做成任务计划程序版本。 diff --git a/deploy/xmonitor-sender.service b/deploy/xmonitor-sender.service new file mode 100644 index 0000000..748ac5b --- /dev/null +++ b/deploy/xmonitor-sender.service @@ -0,0 +1,25 @@ +# Install this file to /etc/systemd/system/xmonitor-sender.service on the target Ubuntu device. +# If you do not use /home/ubuntu/xMonitor, update User, WorkingDirectory, ExecStart, and APP_DIR together. + +[Unit] +Description=xMonitor Robot Status Sender +After=NetworkManager.service +Wants=NetworkManager.service +StartLimitIntervalSec=0 + +[Service] +Type=simple +User=ubuntu +WorkingDirectory=/home/ubuntu/xMonitor +EnvironmentFile=/etc/xmonitor/xmonitor.env +Environment=PYTHONUNBUFFERED=1 +ExecStart=/home/ubuntu/xMonitor/scripts/start_monitor_sender.sh +Restart=always +RestartSec=5 +TimeoutStartSec=0 +StandardOutput=journal +StandardError=journal +SyslogIdentifier=xmonitor-sender + +[Install] +WantedBy=multi-user.target diff --git a/deploy/xmonitor.env.example b/deploy/xmonitor.env.example new file mode 100644 index 0000000..d5df7b4 --- /dev/null +++ b/deploy/xmonitor.env.example @@ -0,0 +1,19 @@ +# Copy this file to /etc/xmonitor/xmonitor.env on the target Ubuntu device. +# Then edit the values and lock down the permissions: +# sudo install -d -m 700 /etc/xmonitor +# sudo cp deploy/xmonitor.env.example /etc/xmonitor/xmonitor.env +# sudo chmod 600 /etc/xmonitor/xmonitor.env + +HOTSPOT_SSID=YOUR_PHONE_HOTSPOT +HOTSPOT_PASSWORD=YOUR_HOTSPOT_PASSWORD +SERVER_IP=10.0.0.5 +SERVER_PORT=9000 +ROS_SETUP=/home/ubuntu/ros2ws/install/setup.bash +APP_DIR=/home/ubuntu/xMonitor +RUN_USER=ubuntu + +# Optional overrides. +PYTHON_BIN=python3 +WIFI_RETRY_INTERVAL=5 +PING_RETRY_INTERVAL=5 +LOG_RETENTION_COUNT=20 diff --git a/scripts/start_monitor_sender.sh b/scripts/start_monitor_sender.sh new file mode 100644 index 0000000..d3c23dd --- /dev/null +++ b/scripts/start_monitor_sender.sh @@ -0,0 +1,208 @@ +#!/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}"