#!/usr/bin/env bash set -Eeuo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" SCRIPT_OWNER="${CAPTURE_USER:-$(stat -c %U "$SCRIPT_DIR")}" SCRIPT_OWNER_HOME="$(getent passwd "$SCRIPT_OWNER" | cut -d: -f6 || true)" cd "$SCRIPT_DIR" ############################ # НАСТРОЙКИ ############################ BASE_DIR="/mnt/data/dataset_6_5_26" PYTHON_BIN="${PYTHON_BIN:-$SCRIPT_DIR/.venv-sdr/bin/python}" SCRIPT_PATH="${SCRIPT_PATH:-$SCRIPT_DIR/scripts_nn/data_saver_headless.py}" ENV_FILE="${ENV_FILE:-$SCRIPT_DIR/.env}" RUN_ONCE="${RUN_ONCE:-0}" CAPTURE_LOG_FILE="${CAPTURE_LOG_FILE:-$BASE_DIR/capture_hourly.log}" SYSTEMCTL_BIN=(systemctl) CURRENT_CAPTURE_PID="" CURRENT_MOCK_PID="" CURRENT_SERVICE_UNIT="" STOPPED_SERVICE_UNIT="" # Лимиты PER_FREQ_LIMIT_GIB="${PER_FREQ_LIMIT_GIB:-12}" TOTAL_LIMIT_GIB="${TOTAL_LIMIT_GIB:-266}" PER_FREQ_LIMIT_BYTES=$((PER_FREQ_LIMIT_GIB * 1024 * 1024 * 1024)) TOTAL_LIMIT_BYTES=$((TOTAL_LIMIT_GIB * 1024 * 1024 * 1024)) # Для обычного hourly можно поставить 3600. # Для RUN_ONCE это почти не важно. CYCLE_SECONDS="${CYCLE_SECONDS:-1}" # Параметры SDR SAMP_RATE="${SAMP_RATE:-20e6}" SPLIT_SIZE="${SPLIT_SIZE:-400000}" DELAY="${DELAY:-0.25}" RF_GAIN="${RF_GAIN:-12}" IF_GAIN="${IF_GAIN:-12}" BB_GAIN="${BB_GAIN:-0}" ############################ # MOCK SENDER ############################ CAPTURE_MOCK_SEND_ENABLED="${CAPTURE_MOCK_SEND_ENABLED:-1}" CAPTURE_MOCK_HOST="${CAPTURE_MOCK_HOST:-127.0.0.1}" CAPTURE_MOCK_INTERVAL_SECONDS="${CAPTURE_MOCK_INTERVAL_SECONDS:-1}" CAPTURE_MOCK_TIMEOUT_SECONDS="${CAPTURE_MOCK_TIMEOUT_SECONDS:-0.3}" CAPTURE_MOCK_LOG_SUCCESS="${CAPTURE_MOCK_LOG_SUCCESS:-0}" ############################ # ВСПОМОГАТЕЛЬНОЕ ############################ log() { printf '[%s] %s\n' "$(date '+%F %T')" "$*" } systemctl_run() { "${SYSTEMCTL_BIN[@]}" "$@" } env_get() { local key="$1" local default="${2:-}" if [[ ! -f "$ENV_FILE" ]]; then printf '%s\n' "$default" return 0 fi awk -v key="$key" -v default="$default" ' BEGIN { found = 0 } $0 ~ "^[[:space:]]*" key "=" { value = $0 sub("^[[:space:]]*" key "=", "", value) sub("[[:space:]]+#.*$", "", value) gsub("^[[:space:]]+|[[:space:]]+$", "", value) if ((substr(value, 1, 1) == "\"" && substr(value, length(value), 1) == "\"") || (substr(value, 1, 1) == "'"'"'" && substr(value, length(value), 1) == "'"'"'")) { value = substr(value, 2, length(value) - 2) } print value found = 1 exit } END { if (!found) { print default } } ' "$ENV_FILE" } mock_bool_enabled() { case "${1,,}" in 1|true|yes|on) return 0 ;; *) return 1 ;; esac } mock_url() { local port endpoint port="${CAPTURE_MOCK_PORT:-$(env_get locport "$(env_get GENERAL_SERVER_PORT 5010)")}" endpoint="${CAPTURE_MOCK_ENDPOINT:-$(env_get freq_endpoint process_data)}" endpoint="${endpoint#/}" printf 'http://%s:%s/%s' "$CAPTURE_MOCK_HOST" "$port" "$endpoint" } service_exists() { local unit="$1" systemctl_run show "$unit" >/dev/null 2>&1 } service_is_active() { local unit="$1" systemctl_run is-active --quiet "$unit" >/dev/null 2>&1 } dir_size_bytes() { local path="$1" if [[ -e "$path" ]]; then du -sb "$path" 2>/dev/null | awk '{print $1}' else echo 0 fi } total_size_bytes() { dir_size_bytes "$BASE_DIR" } terminate_pid() { local pid="${1:-}" if [[ -z "$pid" ]]; then return 0 fi if ! kill -0 "$pid" 2>/dev/null; then return 0 fi kill -TERM "$pid" 2>/dev/null || true for _ in {1..10}; do if ! kill -0 "$pid" 2>/dev/null; then break fi sleep 1 done if kill -0 "$pid" 2>/dev/null; then log "PID=$pid не завершился по TERM, отправляю KILL" kill -KILL "$pid" 2>/dev/null || true fi wait "$pid" 2>/dev/null || true } stop_band_service() { local band="$1" local unit="${SERVICE_UNIT[$band]:-}" STOPPED_SERVICE_UNIT="" CURRENT_SERVICE_UNIT="" if [[ -z "$unit" ]]; then log "Для band=$band не найден service unit" return 0 fi if ! service_exists "$unit"; then log "Service unit $unit не установлен, пропускаю stop/start" return 0 fi log "Останавливаю service $unit перед записью band=$band" if systemctl_run stop "$unit"; then log "Service $unit остановлен или уже был остановлен" else log "Не удалось остановить service $unit" fi STOPPED_SERVICE_UNIT="$unit" CURRENT_SERVICE_UNIT="$unit" return 0 } start_stopped_service() { local unit="${STOPPED_SERVICE_UNIT:-}" if [[ -z "$unit" ]]; then return 0 fi log "Запускаю service $unit после записи" if systemctl_run start "$unit"; then log "Service $unit запущен" else log "Не удалось запустить service $unit" systemctl_run status "$unit" --no-pager || true fi STOPPED_SERVICE_UNIT="" CURRENT_SERVICE_UNIT="" } start_mock_sender() { local band="$1" local log_file="$2" local url CURRENT_MOCK_PID="" if ! mock_bool_enabled "$CAPTURE_MOCK_SEND_ENABLED"; then log "Mock sender отключен CAPTURE_MOCK_SEND_ENABLED=$CAPTURE_MOCK_SEND_ENABLED" return 0 fi url="$(mock_url)" log "Старт mock sender band=$band url=$url amplitude=0 interval=${CAPTURE_MOCK_INTERVAL_SECONDS}s" "$PYTHON_BIN" - \ "$url" \ "$band" \ "$CAPTURE_MOCK_INTERVAL_SECONDS" \ "$CAPTURE_MOCK_TIMEOUT_SECONDS" \ "$CAPTURE_MOCK_LOG_SUCCESS" \ >>"$log_file" 2>&1 <<'PY' & import json import sys import time import urllib.request url = sys.argv[1] freq = str(sys.argv[2]) interval = float(sys.argv[3]) timeout = float(sys.argv[4]) log_success = sys.argv[5].lower() in {"1", "true", "yes", "on"} payload = json.dumps({"freq": freq, "amplitude": 0}).encode("utf-8") headers = {"Content-Type": "application/json"} print( f"[capture-mock] started url={url} freq={freq} amplitude=0 interval={interval}", flush=True, ) while True: try: req = urllib.request.Request( url, data=payload, headers=headers, method="POST", ) with urllib.request.urlopen(req, timeout=timeout) as response: if log_success: print(f"[capture-mock] sent status={response.status}", flush=True) except Exception as exc: print(f"[capture-mock] send failed: {exc}", flush=True) time.sleep(interval) PY CURRENT_MOCK_PID=$! log "Mock sender PID=$CURRENT_MOCK_PID" } stop_mock_sender() { local pid="${1:-}" if [[ -z "$pid" ]]; then return 0 fi if kill -0 "$pid" 2>/dev/null; then log "Останавливаю mock sender PID=$pid" terminate_pid "$pid" fi } cleanup_capture() { local rc=$? if [[ -n "${CURRENT_CAPTURE_PID:-}" ]] && kill -0 "$CURRENT_CAPTURE_PID" 2>/dev/null; then log "Останавливаю текущий capture PID=$CURRENT_CAPTURE_PID" terminate_pid "$CURRENT_CAPTURE_PID" fi CURRENT_CAPTURE_PID="" if [[ -n "${CURRENT_MOCK_PID:-}" ]] && kill -0 "$CURRENT_MOCK_PID" 2>/dev/null; then log "Останавливаю текущий mock sender PID=$CURRENT_MOCK_PID" terminate_pid "$CURRENT_MOCK_PID" fi CURRENT_MOCK_PID="" start_stopped_service exit "$rc" } on_signal() { local sig="$1" log "Получен сигнал $sig, завершаю работу" exit 130 } ############################ # ЧАСТОТЫ И SERIAL ############################ ORDER=(2400) declare -A SERIAL declare -A FREQ_HZ declare -A SERVICE_UNIT SERIAL[433]="$(env_get hack_433)" FREQ_HZ[433]="433000000" SERIAL[750]="$(env_get hack_750)" FREQ_HZ[750]="750000000" SERIAL[868]="$(env_get hack_868)" FREQ_HZ[868]="868000000" SERIAL[915]="$(env_get hack_915)" FREQ_HZ[915]="915000000" SERIAL[1200]="$(env_get hack_1200)" FREQ_HZ[1200]="1200000000" SERIAL[2400]="$(env_get hack_2400)" FREQ_HZ[2400]="2400000000" SERIAL[3300]="$(env_get hack_3300)" FREQ_HZ[3300]="3300000000" SERIAL[4500]="$(env_get hack_4500)" FREQ_HZ[4500]="4500000000" SERIAL[5200]="$(env_get hack_5200)" FREQ_HZ[5200]="5200000000" SERIAL[5800]="$(env_get hack_5800)" FREQ_HZ[5800]="5800000000" SERVICE_UNIT[433]="dronedetector-sdr-433.service" SERVICE_UNIT[750]="dronedetector-sdr-750.service" SERVICE_UNIT[868]="dronedetector-sdr-868.service" SERVICE_UNIT[915]="dronedetector-sdr-915.service" SERVICE_UNIT[1200]="dronedetector-sdr-1200.service" SERVICE_UNIT[2400]="dronedetector-sdr-2400.service" SERVICE_UNIT[3300]="dronedetector-sdr-3300.service" SERVICE_UNIT[4500]="dronedetector-sdr-4500.service" SERVICE_UNIT[5200]="dronedetector-sdr-5200.service" SERVICE_UNIT[5800]="dronedetector-sdr-5800.service" ############################ # ОСНОВНАЯ ЛОГИКА ############################ ensure_requirements() { if [[ ! -x "$PYTHON_BIN" ]]; then echo "Не найден python: $PYTHON_BIN" >&2 exit 1 fi if [[ ! -f "$SCRIPT_PATH" ]]; then echo "Не найден script: $SCRIPT_PATH" >&2 exit 1 fi if ! command -v systemctl >/dev/null 2>&1; then echo "Не найден systemctl" >&2 exit 1 fi if [[ "${EUID}" -ne 0 ]]; then if command -v sudo >/dev/null 2>&1; then SYSTEMCTL_BIN=(sudo systemctl) else echo "Для управления service нужен root или sudo" >&2 exit 1 fi fi mkdir -p "$BASE_DIR" mkdir -p "$(dirname "$CAPTURE_LOG_FILE")" if [[ -n "${CAPTURE_ORDER:-}" ]]; then ORDER=() local band for band in ${CAPTURE_ORDER//,/ }; do ORDER+=("$band") done fi if [[ "${#ORDER[@]}" -eq 0 ]]; then echo "ORDER пустой. Укажите частоты в ORDER или задайте CAPTURE_ORDER." >&2 exit 1 fi log "Рабочая директория: $SCRIPT_DIR" log "BASE_DIR: $BASE_DIR" log "ORDER: ${ORDER[*]}" log "RUN_ONCE: $RUN_ONCE" log "PER_FREQ_LIMIT_GIB: $PER_FREQ_LIMIT_GIB" log "TOTAL_LIMIT_GIB: $TOTAL_LIMIT_GIB" log "CYCLE_SECONDS: $CYCLE_SECONDS" log "CAPTURE_MOCK_SEND_ENABLED: $CAPTURE_MOCK_SEND_ENABLED" log "PYTHON_BIN: $PYTHON_BIN" log "SCRIPT_PATH: $SCRIPT_PATH" } run_one_freq() { local band="$1" local serial="${SERIAL[$band]:-}" local freq="${FREQ_HZ[$band]:-}" if [[ -z "$serial" ]]; then log "Для band=$band пустой serial, пропускаю" return 0 fi if [[ -z "$freq" ]]; then log "Для band=$band не задана частота, пропускаю" return 0 fi local ts out_dir log_file ts="$(date '+%F_%H-%M-%S')" out_dir="$BASE_DIR/$band/$ts" log_file="$out_dir/run.log" log "Старт band=$band serial=$serial freq=$freq dir=$out_dir" if ! mkdir -p "$out_dir"; then log "Не удалось создать каталог $out_dir" return 1 fi stop_band_service "$band" "$PYTHON_BIN" "$SCRIPT_PATH" \ --serial "$serial" \ --freq "$freq" \ --save-dir "$out_dir" \ --file-tag "${band}_" \ --samp-rate "$SAMP_RATE" \ --split-size "$SPLIT_SIZE" \ --delay "$DELAY" \ --rf-gain "$RF_GAIN" \ --if-gain "$IF_GAIN" \ --bb-gain "$BB_GAIN" \ >"$log_file" 2>&1 & local pid=$! CURRENT_CAPTURE_PID="$pid" log "Capture PID=$pid" start_mock_sender "$band" "$log_file" local mock_pid="$CURRENT_MOCK_PID" while kill -0 "$pid" 2>/dev/null; do local cur_dir_size cur_total_size cur_dir_size="$(dir_size_bytes "$out_dir")" cur_total_size="$(total_size_bytes)" if (( cur_total_size >= TOTAL_LIMIT_BYTES )); then log "Достигнут общий лимит ${TOTAL_LIMIT_GIB} GiB. Останавливаю PID=$pid" terminate_pid "$pid" CURRENT_CAPTURE_PID="" stop_mock_sender "$mock_pid" CURRENT_MOCK_PID="" start_stopped_service return 2 fi if (( cur_dir_size >= PER_FREQ_LIMIT_BYTES )); then log "Для band=$band достигнут лимит ${PER_FREQ_LIMIT_GIB} GiB. Останавливаю PID=$pid" terminate_pid "$pid" CURRENT_CAPTURE_PID="" break fi sleep 1 done wait "$pid" 2>/dev/null || true CURRENT_CAPTURE_PID="" stop_mock_sender "$mock_pid" CURRENT_MOCK_PID="" start_stopped_service log "Завершен band=$band, размер=$(du -sh "$out_dir" | awk '{print $1}')" return 0 } main_loop() { while true; do local total_before cycle_start elapsed sleep_left total_before="$(total_size_bytes)" if (( total_before >= TOTAL_LIMIT_BYTES )); then log "Общий размер уже >= ${TOTAL_LIMIT_GIB} GiB, выхожу" break fi cycle_start="$(date +%s)" log "Новый цикл" local band for band in "${ORDER[@]}"; do if (( $(total_size_bytes) >= TOTAL_LIMIT_BYTES )); then log "Общий лимит достигнут внутри цикла, выхожу" return 0 fi run_one_freq "$band" || { local rc=$? if [[ "$rc" -eq 2 ]]; then log "Остановка по общему лимиту" return 0 fi log "Ошибка записи band=$band rc=$rc" return "$rc" } done if [[ "$RUN_ONCE" == "1" ]]; then log "RUN_ONCE=1, завершаю работу после одного цикла" break fi elapsed=$(( $(date +%s) - cycle_start )) sleep_left=$(( CYCLE_SECONDS - elapsed )) if (( sleep_left > 0 )); then log "Цикл занял ${elapsed} сек, жду ${sleep_left} сек" sleep "$sleep_left" else log "Цикл занял ${elapsed} сек, паузы нет" fi done } trap cleanup_capture EXIT trap 'on_signal INT' INT trap 'on_signal TERM' TERM ensure_requirements main_loop