You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
DroneDetector/capture_hourly.sh

593 lines
14 KiB
Bash

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

#!/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="${BASE_DIR:-/mnt/nvme1/dataset_5_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:-380}"
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:-100}"
# Параметры SDR
SAMP_RATE="${SAMP_RATE:-20e6}"
SPLIT_SIZE="${SPLIT_SIZE:-400000}"
DELAY="${DELAY:-0.15}"
RF_GAIN="${RF_GAIN:-12}"
IF_GAIN="${IF_GAIN:-30}"
BB_GAIN="${BB_GAIN:-36}"
############################
# 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=(1200 2400 5200 5800)
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