#!/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)" cd "$SCRIPT_DIR" ENV_FILE="$SCRIPT_DIR/.env" declare -A DOTENV_VALUES trim_whitespace() { local value="$1" value="${value#"${value%%[![:space:]]*}"}" value="${value%"${value##*[![:space:]]}"}" printf '%s' "$value" } load_env_file() { local line key value quote_char if [[ ! -f "$ENV_FILE" ]]; then echo "Не найден .env: $ENV_FILE" >&2 exit 1 fi while IFS= read -r line || [[ -n "$line" ]]; do line="${line%$'\r'}" line="$(trim_whitespace "$line")" [[ -z "$line" || "$line" == \#* ]] && continue if [[ "$line" == export\ * ]]; then line="${line#export }" fi [[ "$line" == *=* ]] || continue key="$(trim_whitespace "${line%%=*}")" value="$(trim_whitespace "${line#*=}")" if [[ ${#value} -ge 2 ]]; then quote_char="${value:0:1}" if [[ ( "$quote_char" == "'" || "$quote_char" == '"' ) && "${value: -1}" == "$quote_char" ]]; then value="${value:1:${#value}-2}" fi fi DOTENV_VALUES["$key"]="$value" done < "$ENV_FILE" } get_env_setting() { local key="$1" local default="${2-}" if [[ -n "${!key+x}" ]]; then printf '%s' "${!key}" elif [[ -n "${DOTENV_VALUES[$key]+x}" ]]; then printf '%s' "${DOTENV_VALUES[$key]}" else printf '%s' "$default" fi } load_env_file ############################ # НАСТРОЙКИ ############################ BASE_DIR="$(get_env_setting CAPTURE_BASE_DIR "${SCRIPT_OWNER_HOME}/dataset/noise")" # Путь к python из venv PYTHON_BIN="$(get_env_setting PYTHON_BIN "$SCRIPT_DIR/.venv-sdr/bin/python")" # Путь к headless скрипту SCRIPT_PATH="$(get_env_setting SCRIPT_PATH "$SCRIPT_DIR/scripts_nn/data_saver_headless.py")" RUN_ONCE="$(get_env_setting RUN_ONCE "0")" CAPTURE_LOG_FILE="$(get_env_setting CAPTURE_LOG_FILE "$BASE_DIR/capture_hourly.log")" SYSTEMCTL_BIN=(systemctl) CURRENT_CAPTURE_PID="" CURRENT_SERVICE_UNITS=() # Лимиты lim_all=10 PER_FREQ_LIMIT_BYTES=$((1 * 1024 * 1024 * 1024)) # GiB на частоту за запуск TOTAL_LIMIT_BYTES=$((lim_all * 1024 * 1024 * 1024)) # общий лимит GiB CYCLE_SECONDS=1 # один цикл в час # Параметры SDR SAMP_RATE="20e6" SPLIT_SIZE="400000" DELAY="0.01" RF_GAIN="12" IF_GAIN="30" BB_GAIN="36" ############################ # ЧАСТОТЫ И SERIAL ИЗ ENV ############################ ORDER=(433) declare -A SERIAL declare -A FREQ_HZ declare -A SERVICE_UNIT SERIAL[433]="$(get_env_setting hack_433)" FREQ_HZ[433]="433000000" SERIAL[750]="$(get_env_setting hack_750)" FREQ_HZ[750]="750000000" SERIAL[915]="$(get_env_setting hack_915)" FREQ_HZ[915]="915000000" SERIAL[1200]="$(get_env_setting hack_1200)" FREQ_HZ[1200]="1200000000" SERIAL[2400]="$(get_env_setting hack_2400)" FREQ_HZ[2400]="2400000000" SERIAL[3300]="$(get_env_setting hack_3300)" FREQ_HZ[3300]="3300000000" SERIAL[4500]="$(get_env_setting hack_4500)" FREQ_HZ[4500]="4500000000" SERIAL[5200]="$(get_env_setting hack_5200)" FREQ_HZ[5200]="5200000000" SERIAL[5800]="$(get_env_setting hack_5800)" FREQ_HZ[5800]="5800000000" SERVICE_UNIT[433]="dronedetector-sdr-433.service" SERVICE_UNIT[750]="dronedetector-sdr-750.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" ############################ # ВСПОМОГАТЕЛЬНОЕ ############################ log() { printf '[%s] %s\n' "$(date '+%F %T')" "$*" } systemctl_run() { "${SYSTEMCTL_BIN[@]}" "$@" } 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" } remember_service_unit() { local unit="$1" local existing for existing in "${CURRENT_SERVICE_UNITS[@]}"; do if [[ "$existing" == "$unit" ]]; then return 0 fi done CURRENT_SERVICE_UNITS+=("$unit") } stop_band_service() { local band="$1" local serial="${SERIAL[$band]:-}" local other_band unit other_serial if [[ -z "$serial" ]]; then log "Для band=$band пустой serial, пропускаю stop/start сервисов" return 0 fi CURRENT_SERVICE_UNITS=() for other_band in "${!SERVICE_UNIT[@]}"; do unit="${SERVICE_UNIT[$other_band]:-}" other_serial="${SERIAL[$other_band]:-}" if [[ -z "$unit" || -z "$other_serial" || "$other_serial" != "$serial" ]]; then continue fi if ! service_exists "$unit"; then log "Service unit $unit не установлен, пропускаю" continue fi if service_is_active "$unit"; then remember_service_unit "$unit" fi log "Останавливаю service $unit перед записью band=$band" systemctl_run stop "$unit" done } start_current_service() { local unit if [[ "${#CURRENT_SERVICE_UNITS[@]}" -eq 0 ]]; then return 0 fi for unit in "${CURRENT_SERVICE_UNITS[@]}"; do log "Запускаю service $unit после записи" systemctl_run start "$unit" done CURRENT_SERVICE_UNITS=() } cleanup_capture() { if [[ -n "$CURRENT_CAPTURE_PID" ]] && kill -0 "$CURRENT_CAPTURE_PID" 2>/dev/null; then log "Останавливаю текущий PID=$CURRENT_CAPTURE_PID при завершении скрипта" kill -TERM "$CURRENT_CAPTURE_PID" 2>/dev/null || true wait "$CURRENT_CAPTURE_PID" 2>/dev/null || true fi CURRENT_CAPTURE_PID="" if [[ "${#CURRENT_SERVICE_UNITS[@]}" -gt 0 ]]; then local unit for unit in "${CURRENT_SERVICE_UNITS[@]}"; do log "Восстанавливаю service $unit при завершении скрипта" systemctl_run start "$unit" || true done CURRENT_SERVICE_UNITS=() fi } on_signal() { local sig="$1" log "Получен сигнал $sig, завершаю работу" exit 130 } 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" } 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")" local capture_order capture_order="$(get_env_setting CAPTURE_ORDER)" 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" } 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 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 "PID=$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 "Достигнут общий лимит $lim_all GiB. Останавливаю PID=$pid" kill -TERM "$pid" 2>/dev/null || true wait "$pid" 2>/dev/null || true CURRENT_CAPTURE_PID="" start_current_service return 2 fi if (( cur_dir_size >= PER_FREQ_LIMIT_BYTES )); then log "Для band=$band достигнут лимит 3 GiB. Останавливаю PID=$pid" 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 break fi sleep 1 done CURRENT_CAPTURE_PID="" start_current_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 "Общий размер уже >= GiB, выхожу" break fi cycle_start="$(date +%s)" log "Новый цикл" for band in "${ORDER[@]}"; do if (( $(total_size_bytes) >= TOTAL_LIMIT_BYTES )); then log "Общий лимит достигнут внутри цикла, выхожу" return 0 fi run_one_freq "$band" || { rc=$? if [[ $rc -eq 2 ]]; then log "Остановка по общему лимиту" return 0 else log "Ошибка записи band=$band rc=$rc" return "$rc" fi } 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