Compare commits

...

10 Commits

1
.gitignore vendored

@ -184,3 +184,4 @@ cython_debug/
*.png
/logs/*.log
runtime/

@ -9,6 +9,7 @@ services:
- ../../.env
environment:
- PYTHONPATH=/app
- JAMMER_STATE_FILE=/app/runtime/jammer_active.flag
working_dir: /app
command: ["python3", "-m", "src.server_to_master"]
restart: unless-stopped
@ -16,6 +17,7 @@ services:
- "5010:5010"
volumes:
- ../../.env:/app/.env:ro
- ../../runtime:/app/runtime
networks:
- dronedetector-net

BIN
out.iq

Binary file not shown.

@ -7,14 +7,14 @@ COMPOSE_FILE="${PROJECT_ROOT}/deploy/docker/docker-compose.yml"
SDR_UNITS=(
dronedetector-sdr-433.service
dronedetector-sdr-750.service
dronedetector-sdr-868.service
#dronedetector-sdr-868.service
dronedetector-sdr-3300.service
dronedetector-sdr-4500.service
#dronedetector-sdr-4500.service
dronedetector-sdr-5200.service
dronedetector-sdr-5800.service
dronedetector-sdr-915.service
dronedetector-sdr-1200.service
dronedetector-sdr-2400.service
#dronedetector-sdr-915.service
#dronedetector-sdr-1200.service
#dronedetector-sdr-2400.service
)
log() {

@ -1,6 +1,6 @@
import os
import math
import statistics
from datetime import datetime
# Более лучшая версия кода есть в FRScanner
@ -13,27 +13,19 @@ class DataBuffer:
current_column: Указатель на текущий столбец буфера, который обновляем.
thinning_counter: Прореживающий множитель на текующей итерации.
current_counter: Указатель на количество чтений между последним обновлением столбца и предыдущим атрибутом.
num_of_thinning_iter: Прореживающий множитель. Раз в это количечество раз будет обнволяться столбец буфера.
num_of_thinning_iter: Прореживающий множитель. Раз в это количество раз будет обновляться столбец буфера.
line_size: Количество строк буфера = количеству каналов.
columns_size: Количество столбцов = фиксированное число.
multiply_factor: Процентный показатель превышения сигналом уровня шума.
multiply_factor: Процентный показатель превышения сигналом уровня шума (legacy).
num_for_alarm: Количество раз, превышающих шум, при которых триггеримся.
is_init: Флаг инициализации буфера. = True, если инициализирован.
is_init: Флаг инициализации буфера.
buffer: Массив для буфера.
buffer_medians: Массив для медиан столбцов букера.
buffer_alarms: Массив для количества тревог по столбца буфера.
buffer_medians: Массив медиан по каналам.
buffer_mads: Массив MAD по каналам.
buffer_alarms: Массив для количества тревог по каналам.
"""
def __init__(self, columns_size, num_of_thinning_iter, num_of_channels, multiply_factor, num_for_alarm):
"""
Инициализируем класс.
:param columns_size:
:param num_of_thinning_iter:
:param num_of_channels:
:param multiply_factor:
:param num_for_alarm:
"""
def __init__(self, columns_size, num_of_thinning_iter, num_of_channels, multiply_factor, num_for_alarm, freq_tag=None):
self.current_column = 0
self.thinning_counter = 1
self.current_counter = 1
@ -43,29 +35,46 @@ class DataBuffer:
self.multiply_factor = multiply_factor
self.num_for_alarm = num_for_alarm
self.is_init = False
self.buffer = [[0 for _ in range(self.columns_size)] for _ in range(self.line_size)]
self.buffer_medians = [0] * self.line_size
self.buffer_timestamps = [[None for _ in range(self.columns_size)] for _ in range(self.line_size)]
self.buffer_medians = [0.0] * self.line_size
self.buffer_mads = [0.0] * self.line_size
self.buffer_alarms = [0] * self.line_size
self.last_alarm_channels = []
self.episode_history = [[0.0 for _ in range(self.num_of_thinning_iter)] for _ in range(self.line_size)]
self.episode_history_timestamps = [[None for _ in range(self.num_of_thinning_iter)] for _ in range(self.line_size)]
self.prev_values = [None] * self.line_size
self.trend_streak = [0] * self.line_size
self.freq_tag = '' if freq_tag is None else str(freq_tag)
suffix = f'_{self.freq_tag}' if self.freq_tag else ''
# Рост в 15% по линейной мощности относительно фоновой медианы в dBFS.
self.dbfs_delta_ratio = float(os.getenv('dbfs_delta_percent', 15)) / 100.0
# Допускаем небольшой обратный ход, чтобы не сбрасываться от микрошума.
self.dbfs_max_backstep_db = float(os.getenv('dbfs_max_backstep_db', 0.25))
# Минимум подряд "плавных" шагов перед учетом как устойчивого роста.
self.dbfs_min_trend_steps = int(os.getenv('dbfs_min_trend_steps', max(1, self.num_for_alarm)))
self.mad_k_on = float(os.getenv('mad_k_on' + suffix, os.getenv('mad_k_on', 5.0)))
self.mad_eps = float(os.getenv('mad_eps' + suffix, os.getenv('mad_eps', 0.05)))
self.dbfs_linear_offset_db = float(
os.getenv('dbfs_linear_offset_db' + suffix, os.getenv('dbfs_linear_offset_db', 0.0))
)
self.dbfs_linear_abs_median_scale = float(
os.getenv('dbfs_linear_abs_median_scale' + suffix, os.getenv('dbfs_linear_abs_median_scale', 0.0))
)
def get_buffer(self):
return self.buffer
def get_timestamps(self):
return self.buffer_timestamps
def get_medians(self):
return self.buffer_medians
def get_mads(self):
return self.buffer_mads
def get_alarms(self):
return self.buffer_alarms
def get_last_alarm_channels(self):
return list(self.last_alarm_channels)
def check_init(self):
return self.is_init
@ -75,25 +84,89 @@ class DataBuffer:
print(self.buffer[i], end=' ')
print()
@staticmethod
def _calc_mad(values, median):
deviations = [abs(v - median) for v in values]
return statistics.median(deviations)
@staticmethod
def _format_ts(timestamp):
if timestamp is None:
return 'None'
try:
return datetime.fromtimestamp(float(timestamp)).isoformat(sep=' ', timespec='milliseconds')
except Exception:
return str(timestamp)
@staticmethod
def _mean_timestamp(timestamps):
filtered = [float(ts) for ts in timestamps if ts is not None]
if not filtered:
return None
return sum(filtered) / len(filtered)
def medians(self):
"""
Вычислить медиану по строке буфера.
Вычислить медиану и MAD по строкам буфера.
:return: None
"""
if self.check_init():
for i in range(self.line_size):
self.buffer_medians[i] = statistics.median(self.buffer[i])
med = float(statistics.median(self.buffer[i]))
self.buffer_medians[i] = med
self.buffer_mads[i] = float(self._calc_mad(self.buffer[i], med))
def get_linear_term(self, median_value):
median_value = float(median_value)
return self.dbfs_linear_offset_db + self.dbfs_linear_abs_median_scale * abs(median_value)
def get_threshold(self, channel_idx):
"""
Получить динамический порог в dB для канала:
threshold = median + linear_term(median) + mad_k_on * MAD.
До завершения инициализации возвращает None.
"""
if not self.check_init():
return None
baseline = float(self.buffer_medians[channel_idx])
mad_eff = max(float(self.buffer_mads[channel_idx]), self.mad_eps)
linear_term = self.get_linear_term(baseline)
return baseline + linear_term + self.mad_k_on * mad_eff
def get_thresholds(self):
if not self.check_init():
return [None] * self.line_size
return [self.get_threshold(i) for i in range(self.line_size)]
def log_threshold_update(self, updated_column):
if not self.check_init():
return
now_str = datetime.now().isoformat(sep=' ', timespec='milliseconds')
freq_tag = self.freq_tag or 'unknown'
print(f'[threshold-update][{freq_tag}] now={now_str} updated_column={updated_column}')
for i in range(self.line_size):
baseline = float(self.buffer_medians[i])
mad = float(self.buffer_mads[i])
mad_eff = max(mad, self.mad_eps)
linear_term = self.get_linear_term(baseline)
threshold = self.get_threshold(i)
packet_times = [self._format_ts(ts) for ts in self.buffer_timestamps[i]]
print(
f' ch={i} median={baseline:.6f} '
f'linear_term={linear_term:.6f} '
f'mad={mad:.6f} mad_eff={mad_eff:.6f} '
f'mad_term={self.mad_k_on * mad_eff:.6f} '
f'threshold={threshold:.6f} '
f'packet_times={packet_times}'
)
def alarms_fill_zeros(self):
self.buffer_alarms = [0] * self.line_size
self.trend_streak = [0] * self.line_size
self.prev_values = [None] * self.line_size
self.last_alarm_channels = []
@staticmethod
def _dbfs_growth_ratio(current_db, baseline_db):
return math.pow(10.0, (current_db - baseline_db) / 10.0) - 1.0
def update(self, data):
def update(self, data, packet_timestamps=None):
"""
Обновление буфера.
Если номер текущего чтения совпадает с количеством прореживающего множителя на текущем обновлении буфера, то
@ -106,77 +179,80 @@ class DataBuffer:
До тех пор, пока множитель на итерации меньше фиксированного, увеличиваем в два раза.
В противном случае - увеличиваем номер чтения.
:param data: Массив с метриками сигнала по каналам.
:param packet_timestamps: Времена пакетов SDR для каждой метрики канала.
:return: None
"""
if packet_timestamps is None:
packet_timestamps = [None] * self.line_size
if len(packet_timestamps) != self.line_size:
raise ValueError('packet_timestamps length must match number of channels')
sample_idx = self.current_counter - 1
for i in range(self.line_size):
self.episode_history[i][sample_idx] = float(data[i])
self.episode_history_timestamps[i][sample_idx] = packet_timestamps[i]
if self.current_counter == self.thinning_counter:
updated_column = self.current_column
for i in range(self.line_size):
self.buffer[i][self.current_column] = data[i]
samples = self.episode_history[i][:self.thinning_counter]
timestamps = self.episode_history_timestamps[i][:self.thinning_counter]
self.buffer[i][self.current_column] = float(sum(samples) / len(samples))
self.buffer_timestamps[i][self.current_column] = self._mean_timestamp(timestamps)
self.current_column = (self.current_column + 1) % self.columns_size
self.medians()
if self.check_init():
self.log_threshold_update(updated_column)
for i in range(self.line_size):
for j in range(self.thinning_counter):
self.episode_history[i][j] = 0.0
self.episode_history_timestamps[i][j] = None
self.current_counter = 1
if self.current_column == 0:
if self.thinning_counter == 1:
self.is_init = True
self.medians()
print('Начальная калибровка завершена.')
self.log_threshold_update(updated_column)
if self.thinning_counter < self.num_of_thinning_iter:
self.thinning_counter *= 2
else:
self.current_counter += 1
def check_alarm(self, data):
"""
Проверка триггера системы по dBFS во времени.
Триггер: устойчивый рост относительно фоновой медианы не меньше dbfs_delta_percent,
подтвержденный несколькими последовательными чтениями.
Один порог на канал, набор тревоги и сброс счетчиков как в main.
"""
if self.check_init():
self.last_alarm_channels = []
for i in range(len(data)):
baseline = self.buffer_medians[i]
current = data[i]
growth_ratio = self._dbfs_growth_ratio(current, baseline)
prev = self.prev_values[i]
delta_db = 0.0 if prev is None else current - prev
monotonic_or_stable = (prev is None) or (delta_db >= -self.dbfs_max_backstep_db)
if monotonic_or_stable:
self.trend_streak[i] += 1
else:
self.trend_streak[i] = 0
exceeding = (
growth_ratio >= self.dbfs_delta_ratio
and self.trend_streak[i] >= self.dbfs_min_trend_steps
)
threshold = self.get_threshold(i)
exceeding = current >= threshold
if exceeding:
self.buffer_alarms[i] += 1
else:
self.buffer_alarms[i] = 0
self.prev_values[i] = current
if self.buffer_alarms[i] >= self.num_for_alarm:
self.last_alarm_channels = [i]
self.buffer_alarms = [0] * self.line_size
self.trend_streak = [0] * self.line_size
return True
return False
def check_single_alarm(self, median, cur_channel):
"""
Проверка, является ли текущая метрика по каналу превышающей порог роста.
Проверка, является ли текущая метрика по каналу превышающей порог.
:param median: текущая метрика в dBFS.
:param cur_channel: индекс канала внутри частоты.
:return: Да/нет.
"""
if self.check_init():
baseline = self.buffer_medians[cur_channel]
exceeding = self._dbfs_growth_ratio(median, baseline) >= self.dbfs_delta_ratio
if exceeding:
return True
else:
threshold = self.get_threshold(cur_channel)
return median >= threshold
return False

@ -155,7 +155,15 @@ class MultiChannel:
num_for_alarm = int(os.getenv('num_for_alarm_' + str(freq)))
num_chs = self.get_num_chs(i)
self.DB.append(
DataBuffer(buffer_columns_size, num_of_thinning_iter, num_chs, multiply_factor, num_for_alarm))
DataBuffer(
buffer_columns_size,
num_of_thinning_iter,
num_chs,
multiply_factor,
num_for_alarm,
freq_tag=str(freq),
)
)
def db_alarms_zeros(self, circle_buffer):
"""

@ -1,4 +1,6 @@
import os
import math
import time
import numpy as np
from typing import Union
from common.runtime import load_root_env
@ -24,6 +26,10 @@ class Signal:
self.conv_method = conv_method
self.signal = []
self.signal_abs = []
self.last_packet_ts = None
self.metric_mode = os.getenv('signal_metric_mode', 'fft_top_bins').strip().lower()
self.fft_top_bins = max(1, int(os.getenv('signal_fft_top_bins', '2048')))
self.fft_window = os.getenv('signal_fft_window', 'hann').strip().lower()
def get_signal(self):
"""
@ -32,6 +38,9 @@ class Signal:
"""
return self.signal, self.signal_abs
def get_last_packet_ts(self):
return self.last_packet_ts
def clear(self) -> None:
"""
Очистить массив с сигналом после предобработки?
@ -39,6 +48,31 @@ class Signal:
"""
self.signal = []
self.signal_abs = []
self.last_packet_ts = None
def _build_window(self, size: int) -> np.ndarray:
if self.fft_window in {'', 'none', 'rect', 'rectangular'}:
return np.ones(size, dtype=np.float32)
if self.fft_window == 'hann':
return np.hanning(size).astype(np.float32, copy=False)
raise ValueError(f'unsupported fft window: {self.fft_window}')
def _compute_iq_power(self, samples: np.ndarray, signal_abs: np.ndarray) -> float:
if self.conv_method == 'max':
return float(np.max(signal_abs * signal_abs))
if self.metric_mode in {'fft', 'fft_top_bins', 'top_bins'}:
window = self._build_window(samples.size)
windowed = samples.astype(np.complex64, copy=False) * window
spectrum = np.fft.fft(windowed)
power_bins = (np.abs(spectrum) ** 2).astype(np.float32, copy=False)
power_bins /= max(float(np.sum(window * window)), 1.0)
bins_to_keep = min(self.fft_top_bins, power_bins.size)
top_bins = np.partition(power_bins, power_bins.size - bins_to_keep)[-bins_to_keep:]
return float(np.mean(top_bins))
return float(np.mean(signal_abs * signal_abs))
def signal_preprocessing(self, length) -> float:
"""
@ -46,17 +80,35 @@ class Signal:
:return: Число типа float - "характеристика сигнала".
"""
signal = np.array([self.signal.real[0:length], self.signal.imag[0:length]], dtype=np.float32)
signal_abs = np.linalg.norm(signal, axis=0) # Поэлементный модуль комплексного числа. shape.result
# (1, self.length)
if self.conv_method == 'max':
result = np.max(signal_abs)
else:
result = np.median(signal_abs)
samples = np.asarray(self.signal).ravel()[0:length]
if samples.size == 0:
return 0.0
# Основной режим: считаем dBFS из IQ-вектора.
if np.iscomplexobj(samples):
i = samples.real.astype(np.float32, copy=False)
q = samples.imag.astype(np.float32, copy=False)
signal = np.array([i, q], dtype=np.float32)
signal_abs = np.sqrt(i * i + q * q).astype(np.float32, copy=False)
power = self._compute_iq_power(samples, signal_abs)
result = 10.0 * math.log10(max(power, 1e-20))
self.signal = signal
self.signal_abs = signal_abs
return result
# Fallback: если на вход уже подали скалярную метрику, агрегируем как есть.
scalar_samples = samples.astype(np.float32, copy=False)
if self.conv_method == 'max':
result = float(np.max(scalar_samples))
else:
result = float(np.median(scalar_samples))
self.signal = scalar_samples
self.signal_abs = np.abs(scalar_samples)
return result
def fill_signal(self, lvl, length) -> Union[int, float]:
"""
Сбор сигнала в соответствующий массив. Если уже собран, то предобработка.
@ -69,8 +121,8 @@ class Signal:
self.signal = np.concatenate((self.signal, y), axis=None)
return 0
else:
self.last_packet_ts = time.time()
preproc_signal = self.signal_preprocessing(length)
#self.clear()
return preproc_signal
@ -81,27 +133,33 @@ class SignalsArray:
sig_array: Список для сохранения медиан.
counter: Индикатор наполненности массива.
"""
def __init__(self):
self.sig_array = []
self.sig_ts_array = []
self.counter = 0
def fill_sig_arr(self, metrica, num_chs=3):
def fill_sig_arr(self, metrica, packet_ts=None, num_chs=3):
"""
Аппендим характеристику сигнала (метрику) в массив длиной num_chs.
:param metrica: Характеристика сигнала (метрика).
:param packet_ts: Время завершения набора пакета с SDR для текущей метрики.
:param num_chs: Количество каналов на частоте.
:return: Индекс канала внутри частоты и массив с характеристиками, если заполнен, иначе - пустой.
"""
if num_chs:
if self.counter < num_chs:
self.sig_array.append(metrica)
self.sig_ts_array.append(packet_ts)
self.counter += 1
if self.counter == num_chs:
arr = self.sig_array
arr_ts = self.sig_ts_array
self.sig_array = []
self.sig_ts_array = []
self.counter = 0
return num_chs - 1, arr
return num_chs - 1, arr, arr_ts
else:
return self.counter - 1, []
return self.counter - 1, [], []
else:
return 0, []
return 0, [], []

@ -1,8 +1,10 @@
import os
import datetime
import time
from common.runtime import load_root_env, validate_env, as_bool, as_str
from smb.SMBConnection import SMBConnection
from utils.datas_processing import pack_elems, agregator, send_data, save_data, remote_save_data
from utils.datas_processing import pack_elems, agregator, send_data, send_telemetry, save_data, remote_save_data
from utils.jammer_state_flag import is_jammer_active
from core.sig_n_medi_collect import Signal, SignalsArray, get_signal_length
from core.multichannelswitcher import MultiChannel, get_centre_freq
@ -46,6 +48,11 @@ the_pc_name = os.getenv('the_pc_name')
remote_pc_name = os.getenv('remote_pc_name')
smb_domain = os.getenv('smb_domain')
freq_endpoint = os.getenv('freq_endpoint')
telemetry_enabled = as_bool(os.getenv('telemetry_enabled', '1'))
telemetry_host = os.getenv('telemetry_host', '127.0.0.1')
telemetry_port = os.getenv('telemetry_port', '5020')
telemetry_endpoint = os.getenv('telemetry_endpoint', 'telemetry')
telemetry_timeout_sec = float(os.getenv('telemetry_timeout_sec', '0.30'))
elems_to_save = elems_to_save.split(',')
file_types_to_save = file_types_to_save.split(',')
@ -69,11 +76,12 @@ def work(lvl):
freq = get_centre_freq(f)
signal_length = get_signal_length(freq)
median = tmp_signal.fill_signal(lvl, signal_length)
packet_ts = tmp_signal.get_last_packet_ts()
if median:
try:
num_chs, circle_buffer = multi_channel.check_f(f)
cur_channel, sigs_array = tmp_sigs_array.fill_sig_arr(median, num_chs)
cur_channel, sigs_array, sigs_ts_array = tmp_sigs_array.fill_sig_arr(median, packet_ts=packet_ts, num_chs=num_chs)
if sigs_array:
print('Значения на {0}: {1}'.format(freq, sigs_array))
@ -83,8 +91,40 @@ def work(lvl):
if alarm:
print('----ALARM---- ', freq)
multi_channel.db_alarms_zeros(circle_buffer)
else:
circle_buffer.update(sigs_array)
elif not is_jammer_active():
circle_buffer.update(sigs_array, packet_timestamps=sigs_ts_array)
if telemetry_enabled:
try:
max_idx = max(range(len(sigs_array)), key=lambda idx: sigs_array[idx])
dbfs_current = float(sigs_array[max_idx])
dbfs_threshold = circle_buffer.get_threshold(max_idx)
channel_thresholds = circle_buffer.get_thresholds()
alarm_channels = circle_buffer.get_last_alarm_channels() if alarm else []
send_telemetry(
data={
"freq": str(freq),
"ts": time.time(),
"dbfs_current": dbfs_current,
"dbfs_threshold": dbfs_threshold,
"alarm": bool(alarm),
"channel_idx": int(max_idx),
"channels_total": int(len(sigs_array)),
"channel_values": [float(v) for v in sigs_array],
"channel_thresholds": channel_thresholds,
"alarm_channels": alarm_channels,
},
host=telemetry_host,
port=telemetry_port,
endpoint=telemetry_endpoint,
timeout_sec=telemetry_timeout_sec,
)
except Exception as exc:
if debug_flag:
print(f"telemetry send failed: {exc}")
if send_to_module_flag:
send_data(agregator(freq, alarm), localhost, localport, freq_endpoint)

@ -1,8 +1,10 @@
import os
import datetime
import time
from common.runtime import load_root_env, validate_env, as_bool, as_str
from smb.SMBConnection import SMBConnection
from utils.datas_processing import pack_elems, agregator, send_data, save_data, remote_save_data
from utils.datas_processing import pack_elems, agregator, send_data, send_telemetry, save_data, remote_save_data
from utils.jammer_state_flag import is_jammer_active
from core.sig_n_medi_collect import Signal, SignalsArray, get_signal_length
from core.multichannelswitcher import MultiChannel, get_centre_freq
@ -46,6 +48,11 @@ the_pc_name = os.getenv('the_pc_name')
remote_pc_name = os.getenv('remote_pc_name')
smb_domain = os.getenv('smb_domain')
freq_endpoint = os.getenv('freq_endpoint')
telemetry_enabled = as_bool(os.getenv('telemetry_enabled', '1'))
telemetry_host = os.getenv('telemetry_host', '127.0.0.1')
telemetry_port = os.getenv('telemetry_port', '5020')
telemetry_endpoint = os.getenv('telemetry_endpoint', 'telemetry')
telemetry_timeout_sec = float(os.getenv('telemetry_timeout_sec', '0.30'))
elems_to_save = elems_to_save.split(',')
file_types_to_save = file_types_to_save.split(',')
@ -69,11 +76,12 @@ def work(lvl):
freq = get_centre_freq(f)
signal_length = get_signal_length(freq)
median = tmp_signal.fill_signal(lvl, signal_length)
packet_ts = tmp_signal.get_last_packet_ts()
if median:
try:
num_chs, circle_buffer = multi_channel.check_f(f)
cur_channel, sigs_array = tmp_sigs_array.fill_sig_arr(median, num_chs)
cur_channel, sigs_array, sigs_ts_array = tmp_sigs_array.fill_sig_arr(median, packet_ts=packet_ts, num_chs=num_chs)
if sigs_array:
print('Значения на {0}: {1}'.format(freq, sigs_array))
@ -83,8 +91,40 @@ def work(lvl):
if alarm:
print('----ALARM---- ', freq)
multi_channel.db_alarms_zeros(circle_buffer)
else:
circle_buffer.update(sigs_array)
elif not is_jammer_active():
circle_buffer.update(sigs_array, packet_timestamps=sigs_ts_array)
if telemetry_enabled:
try:
max_idx = max(range(len(sigs_array)), key=lambda idx: sigs_array[idx])
dbfs_current = float(sigs_array[max_idx])
dbfs_threshold = circle_buffer.get_threshold(max_idx)
channel_thresholds = circle_buffer.get_thresholds()
alarm_channels = circle_buffer.get_last_alarm_channels() if alarm else []
send_telemetry(
data={
"freq": str(freq),
"ts": time.time(),
"dbfs_current": dbfs_current,
"dbfs_threshold": dbfs_threshold,
"alarm": bool(alarm),
"channel_idx": int(max_idx),
"channels_total": int(len(sigs_array)),
"channel_values": [float(v) for v in sigs_array],
"channel_thresholds": channel_thresholds,
"alarm_channels": alarm_channels,
},
host=telemetry_host,
port=telemetry_port,
endpoint=telemetry_endpoint,
timeout_sec=telemetry_timeout_sec,
)
except Exception as exc:
if debug_flag:
print(f"telemetry send failed: {exc}")
if send_to_module_flag:
send_data(agregator(freq, alarm), localhost, localport, freq_endpoint)

@ -1,8 +1,10 @@
import os
import datetime
import time
from common.runtime import load_root_env, validate_env, as_bool, as_str
from smb.SMBConnection import SMBConnection
from utils.datas_processing import pack_elems, agregator, send_data, save_data, remote_save_data
from utils.datas_processing import pack_elems, agregator, send_data, send_telemetry, save_data, remote_save_data
from utils.jammer_state_flag import is_jammer_active
from core.sig_n_medi_collect import Signal, SignalsArray, get_signal_length
from core.multichannelswitcher import MultiChannel, get_centre_freq
@ -46,6 +48,11 @@ the_pc_name = os.getenv('the_pc_name')
remote_pc_name = os.getenv('remote_pc_name')
smb_domain = os.getenv('smb_domain')
freq_endpoint = os.getenv('freq_endpoint')
telemetry_enabled = as_bool(os.getenv('telemetry_enabled', '1'))
telemetry_host = os.getenv('telemetry_host', '127.0.0.1')
telemetry_port = os.getenv('telemetry_port', '5020')
telemetry_endpoint = os.getenv('telemetry_endpoint', 'telemetry')
telemetry_timeout_sec = float(os.getenv('telemetry_timeout_sec', '0.30'))
elems_to_save = elems_to_save.split(',')
file_types_to_save = file_types_to_save.split(',')
@ -69,11 +76,12 @@ def work(lvl):
freq = get_centre_freq(f)
signal_length = get_signal_length(freq)
median = tmp_signal.fill_signal(lvl, signal_length)
packet_ts = tmp_signal.get_last_packet_ts()
if median:
try:
num_chs, circle_buffer = multi_channel.check_f(f)
cur_channel, sigs_array = tmp_sigs_array.fill_sig_arr(median, num_chs)
cur_channel, sigs_array, sigs_ts_array = tmp_sigs_array.fill_sig_arr(median, packet_ts=packet_ts, num_chs=num_chs)
if sigs_array:
print('Значения на {0}: {1}'.format(freq, sigs_array))
@ -83,8 +91,40 @@ def work(lvl):
if alarm:
print('----ALARM---- ', freq)
multi_channel.db_alarms_zeros(circle_buffer)
else:
circle_buffer.update(sigs_array)
elif not is_jammer_active():
circle_buffer.update(sigs_array, packet_timestamps=sigs_ts_array)
if telemetry_enabled:
try:
max_idx = max(range(len(sigs_array)), key=lambda idx: sigs_array[idx])
dbfs_current = float(sigs_array[max_idx])
dbfs_threshold = circle_buffer.get_threshold(max_idx)
channel_thresholds = circle_buffer.get_thresholds()
alarm_channels = circle_buffer.get_last_alarm_channels() if alarm else []
send_telemetry(
data={
"freq": str(freq),
"ts": time.time(),
"dbfs_current": dbfs_current,
"dbfs_threshold": dbfs_threshold,
"alarm": bool(alarm),
"channel_idx": int(max_idx),
"channels_total": int(len(sigs_array)),
"channel_values": [float(v) for v in sigs_array],
"channel_thresholds": channel_thresholds,
"alarm_channels": alarm_channels,
},
host=telemetry_host,
port=telemetry_port,
endpoint=telemetry_endpoint,
timeout_sec=telemetry_timeout_sec,
)
except Exception as exc:
if debug_flag:
print(f"telemetry send failed: {exc}")
if send_to_module_flag:
send_data(agregator(freq, alarm), localhost, localport, freq_endpoint)

@ -1,8 +1,10 @@
import os
import datetime
import time
from common.runtime import load_root_env, validate_env, as_bool, as_str
from smb.SMBConnection import SMBConnection
from utils.datas_processing import pack_elems, agregator, send_data, save_data, remote_save_data
from utils.datas_processing import pack_elems, agregator, send_data, send_telemetry, save_data, remote_save_data
from utils.jammer_state_flag import is_jammer_active
from core.sig_n_medi_collect import Signal, SignalsArray, get_signal_length
from core.multichannelswitcher import MultiChannel, get_centre_freq
@ -46,6 +48,11 @@ the_pc_name = os.getenv('the_pc_name')
remote_pc_name = os.getenv('remote_pc_name')
smb_domain = os.getenv('smb_domain')
freq_endpoint = os.getenv('freq_endpoint')
telemetry_enabled = as_bool(os.getenv('telemetry_enabled', '1'))
telemetry_host = os.getenv('telemetry_host', '127.0.0.1')
telemetry_port = os.getenv('telemetry_port', '5020')
telemetry_endpoint = os.getenv('telemetry_endpoint', 'telemetry')
telemetry_timeout_sec = float(os.getenv('telemetry_timeout_sec', '0.30'))
elems_to_save = elems_to_save.split(',')
file_types_to_save = file_types_to_save.split(',')
@ -69,11 +76,12 @@ def work(lvl):
freq = get_centre_freq(f)
signal_length = get_signal_length(freq)
median = tmp_signal.fill_signal(lvl, signal_length)
packet_ts = tmp_signal.get_last_packet_ts()
if median:
try:
num_chs, circle_buffer = multi_channel.check_f(f)
cur_channel, sigs_array = tmp_sigs_array.fill_sig_arr(median, num_chs)
cur_channel, sigs_array, sigs_ts_array = tmp_sigs_array.fill_sig_arr(median, packet_ts=packet_ts, num_chs=num_chs)
if sigs_array:
print('Значения на {0}: {1}'.format(freq, sigs_array))
@ -83,8 +91,40 @@ def work(lvl):
if alarm:
print('----ALARM---- ', freq)
multi_channel.db_alarms_zeros(circle_buffer)
else:
circle_buffer.update(sigs_array)
elif not is_jammer_active():
circle_buffer.update(sigs_array, packet_timestamps=sigs_ts_array)
if telemetry_enabled:
try:
max_idx = max(range(len(sigs_array)), key=lambda idx: sigs_array[idx])
dbfs_current = float(sigs_array[max_idx])
dbfs_threshold = circle_buffer.get_threshold(max_idx)
channel_thresholds = circle_buffer.get_thresholds()
alarm_channels = circle_buffer.get_last_alarm_channels() if alarm else []
send_telemetry(
data={
"freq": str(freq),
"ts": time.time(),
"dbfs_current": dbfs_current,
"dbfs_threshold": dbfs_threshold,
"alarm": bool(alarm),
"channel_idx": int(max_idx),
"channels_total": int(len(sigs_array)),
"channel_values": [float(v) for v in sigs_array],
"channel_thresholds": channel_thresholds,
"alarm_channels": alarm_channels,
},
host=telemetry_host,
port=telemetry_port,
endpoint=telemetry_endpoint,
timeout_sec=telemetry_timeout_sec,
)
except Exception as exc:
if debug_flag:
print(f"telemetry send failed: {exc}")
if send_to_module_flag:
send_data(agregator(freq, alarm), localhost, localport, freq_endpoint)

@ -1,8 +1,10 @@
import os
import datetime
import time
from common.runtime import load_root_env, validate_env, as_bool, as_str
from smb.SMBConnection import SMBConnection
from utils.datas_processing import pack_elems, agregator, send_data, save_data, remote_save_data
from utils.datas_processing import pack_elems, agregator, send_data, send_telemetry, save_data, remote_save_data
from utils.jammer_state_flag import is_jammer_active
from core.sig_n_medi_collect import Signal, SignalsArray, get_signal_length
from core.multichannelswitcher import MultiChannel, get_centre_freq
@ -46,6 +48,11 @@ the_pc_name = os.getenv('the_pc_name')
remote_pc_name = os.getenv('remote_pc_name')
smb_domain = os.getenv('smb_domain')
freq_endpoint = os.getenv('freq_endpoint')
telemetry_enabled = as_bool(os.getenv('telemetry_enabled', '1'))
telemetry_host = os.getenv('telemetry_host', '127.0.0.1')
telemetry_port = os.getenv('telemetry_port', '5020')
telemetry_endpoint = os.getenv('telemetry_endpoint', 'telemetry')
telemetry_timeout_sec = float(os.getenv('telemetry_timeout_sec', '0.30'))
elems_to_save = elems_to_save.split(',')
file_types_to_save = file_types_to_save.split(',')
@ -69,11 +76,12 @@ def work(lvl):
freq = get_centre_freq(f)
signal_length = get_signal_length(freq)
median = tmp_signal.fill_signal(lvl, signal_length)
packet_ts = tmp_signal.get_last_packet_ts()
if median:
try:
num_chs, circle_buffer = multi_channel.check_f(f)
cur_channel, sigs_array = tmp_sigs_array.fill_sig_arr(median, num_chs)
cur_channel, sigs_array, sigs_ts_array = tmp_sigs_array.fill_sig_arr(median, packet_ts=packet_ts, num_chs=num_chs)
if sigs_array:
print('Значения на {0}: {1}'.format(freq, sigs_array))
@ -83,8 +91,40 @@ def work(lvl):
if alarm:
print('----ALARM---- ', freq)
multi_channel.db_alarms_zeros(circle_buffer)
else:
circle_buffer.update(sigs_array)
elif not is_jammer_active():
circle_buffer.update(sigs_array, packet_timestamps=sigs_ts_array)
if telemetry_enabled:
try:
max_idx = max(range(len(sigs_array)), key=lambda idx: sigs_array[idx])
dbfs_current = float(sigs_array[max_idx])
dbfs_threshold = circle_buffer.get_threshold(max_idx)
channel_thresholds = circle_buffer.get_thresholds()
alarm_channels = circle_buffer.get_last_alarm_channels() if alarm else []
send_telemetry(
data={
"freq": str(freq),
"ts": time.time(),
"dbfs_current": dbfs_current,
"dbfs_threshold": dbfs_threshold,
"alarm": bool(alarm),
"channel_idx": int(max_idx),
"channels_total": int(len(sigs_array)),
"channel_values": [float(v) for v in sigs_array],
"channel_thresholds": channel_thresholds,
"alarm_channels": alarm_channels,
},
host=telemetry_host,
port=telemetry_port,
endpoint=telemetry_endpoint,
timeout_sec=telemetry_timeout_sec,
)
except Exception as exc:
if debug_flag:
print(f"telemetry send failed: {exc}")
if send_to_module_flag:
send_data(agregator(freq, alarm), localhost, localport, freq_endpoint)

@ -1,8 +1,10 @@
import os
import datetime
import time
from common.runtime import load_root_env, validate_env, as_bool, as_str
from smb.SMBConnection import SMBConnection
from utils.datas_processing import pack_elems, agregator, send_data, save_data, remote_save_data
from utils.datas_processing import pack_elems, agregator, send_data, send_telemetry, save_data, remote_save_data
from utils.jammer_state_flag import is_jammer_active
from core.sig_n_medi_collect import Signal, SignalsArray, get_signal_length
from core.multichannelswitcher import MultiChannel, get_centre_freq
@ -46,6 +48,11 @@ the_pc_name = os.getenv('the_pc_name')
remote_pc_name = os.getenv('remote_pc_name')
smb_domain = os.getenv('smb_domain')
freq_endpoint = os.getenv('freq_endpoint')
telemetry_enabled = as_bool(os.getenv('telemetry_enabled', '1'))
telemetry_host = os.getenv('telemetry_host', '127.0.0.1')
telemetry_port = os.getenv('telemetry_port', '5020')
telemetry_endpoint = os.getenv('telemetry_endpoint', 'telemetry')
telemetry_timeout_sec = float(os.getenv('telemetry_timeout_sec', '0.30'))
elems_to_save = elems_to_save.split(',')
file_types_to_save = file_types_to_save.split(',')
@ -69,13 +76,14 @@ def work(lvl):
freq = get_centre_freq(f)
signal_length = get_signal_length(freq)
median = tmp_signal.fill_signal(lvl, signal_length)
packet_ts = tmp_signal.get_last_packet_ts()
if median:
print(1)
try:
num_chs, circle_buffer = multi_channel.check_f(f)
print(num_chs, circle_buffer)
cur_channel, sigs_array = tmp_sigs_array.fill_sig_arr(median, num_chs)
cur_channel, sigs_array, sigs_ts_array = tmp_sigs_array.fill_sig_arr(median, packet_ts=packet_ts, num_chs=num_chs)
print(3)
if sigs_array:
@ -86,8 +94,40 @@ def work(lvl):
if alarm:
print('----ALARM---- ', freq)
multi_channel.db_alarms_zeros(circle_buffer)
else:
circle_buffer.update(sigs_array)
elif not is_jammer_active():
circle_buffer.update(sigs_array, packet_timestamps=sigs_ts_array)
if telemetry_enabled:
try:
max_idx = max(range(len(sigs_array)), key=lambda idx: sigs_array[idx])
dbfs_current = float(sigs_array[max_idx])
dbfs_threshold = circle_buffer.get_threshold(max_idx)
channel_thresholds = circle_buffer.get_thresholds()
alarm_channels = circle_buffer.get_last_alarm_channels() if alarm else []
send_telemetry(
data={
"freq": str(freq),
"ts": time.time(),
"dbfs_current": dbfs_current,
"dbfs_threshold": dbfs_threshold,
"alarm": bool(alarm),
"channel_idx": int(max_idx),
"channels_total": int(len(sigs_array)),
"channel_values": [float(v) for v in sigs_array],
"channel_thresholds": channel_thresholds,
"alarm_channels": alarm_channels,
},
host=telemetry_host,
port=telemetry_port,
endpoint=telemetry_endpoint,
timeout_sec=telemetry_timeout_sec,
)
except Exception as exc:
if debug_flag:
print(f"telemetry send failed: {exc}")
if send_to_module_flag:
send_data(agregator(freq, alarm), localhost, localport, freq_endpoint)

@ -1,8 +1,10 @@
import os
import datetime
import time
from common.runtime import load_root_env, validate_env, as_bool, as_str
from smb.SMBConnection import SMBConnection
from utils.datas_processing import pack_elems, agregator, send_data, save_data, remote_save_data
from utils.datas_processing import pack_elems, agregator, send_data, send_telemetry, save_data, remote_save_data
from utils.jammer_state_flag import is_jammer_active
from core.sig_n_medi_collect import Signal, SignalsArray, get_signal_length
from core.multichannelswitcher import MultiChannel, get_centre_freq
@ -46,6 +48,11 @@ the_pc_name = os.getenv('the_pc_name')
remote_pc_name = os.getenv('remote_pc_name')
smb_domain = os.getenv('smb_domain')
freq_endpoint = os.getenv('freq_endpoint')
telemetry_enabled = as_bool(os.getenv('telemetry_enabled', '1'))
telemetry_host = os.getenv('telemetry_host', '127.0.0.1')
telemetry_port = os.getenv('telemetry_port', '5020')
telemetry_endpoint = os.getenv('telemetry_endpoint', 'telemetry')
telemetry_timeout_sec = float(os.getenv('telemetry_timeout_sec', '0.30'))
elems_to_save = elems_to_save.split(',')
file_types_to_save = file_types_to_save.split(',')
@ -69,11 +76,12 @@ def work(lvl):
freq = get_centre_freq(f)
signal_length = get_signal_length(freq)
median = tmp_signal.fill_signal(lvl, signal_length)
packet_ts = tmp_signal.get_last_packet_ts()
if median:
try:
num_chs, circle_buffer = multi_channel.check_f(f)
cur_channel, sigs_array = tmp_sigs_array.fill_sig_arr(median, num_chs)
cur_channel, sigs_array, sigs_ts_array = tmp_sigs_array.fill_sig_arr(median, packet_ts=packet_ts, num_chs=num_chs)
if sigs_array:
print('Значения на {0}: {1}'.format(freq, sigs_array))
@ -83,8 +91,40 @@ def work(lvl):
if alarm:
print('----ALARM---- ', freq)
multi_channel.db_alarms_zeros(circle_buffer)
else:
circle_buffer.update(sigs_array)
elif not is_jammer_active():
circle_buffer.update(sigs_array, packet_timestamps=sigs_ts_array)
if telemetry_enabled:
try:
max_idx = max(range(len(sigs_array)), key=lambda idx: sigs_array[idx])
dbfs_current = float(sigs_array[max_idx])
dbfs_threshold = circle_buffer.get_threshold(max_idx)
channel_thresholds = circle_buffer.get_thresholds()
alarm_channels = circle_buffer.get_last_alarm_channels() if alarm else []
send_telemetry(
data={
"freq": str(freq),
"ts": time.time(),
"dbfs_current": dbfs_current,
"dbfs_threshold": dbfs_threshold,
"alarm": bool(alarm),
"channel_idx": int(max_idx),
"channels_total": int(len(sigs_array)),
"channel_values": [float(v) for v in sigs_array],
"channel_thresholds": channel_thresholds,
"alarm_channels": alarm_channels,
},
host=telemetry_host,
port=telemetry_port,
endpoint=telemetry_endpoint,
timeout_sec=telemetry_timeout_sec,
)
except Exception as exc:
if debug_flag:
print(f"telemetry send failed: {exc}")
if send_to_module_flag:
send_data(agregator(freq, alarm), localhost, localport, freq_endpoint)

@ -14,6 +14,7 @@ from fastapi import FastAPI
from common.runtime import load_root_env, validate_env, as_bool, as_float, as_int, as_str
from datetime import datetime, timedelta
import logging
from src.utils.jammer_state_flag import set_jammer_active
logging.basicConfig(level=logging.INFO)
@ -416,6 +417,7 @@ async def jammer_active():
freqs_alarm = {freq: 0 for freq in freqs}
jammer_event = True
set_jammer_active(True)
print('АКТИВИРУЕМ ПОДАВИТЕЛЬ ААААААААААААААААААААААААААААААААААААААААААААААА!!!!')
print('-' * 20)
@ -438,6 +440,7 @@ async def jammer_deactive():
global sending_data_task
alarm = False
jammer_event = False
set_jammer_active(False)
sending_data_task = asyncio.create_task(sending_data())
print('ОТКЛЮАЕМ ПОДАВИТЕЛЬ ААААААААААААААААААААААААААААААААААААААААААААААААА!!!!')
@ -496,6 +499,7 @@ async def jam_server():
await jammer_deactive()
except Exception as e:
jam_server_connect = None
set_jammer_active(False)
if jammer_event:
await jammer_deactive()
@ -508,6 +512,7 @@ async def startup_event():
"""
global sending_data_task
set_jammer_active(False)
asyncio.create_task(jam_server())
sending_data_task = asyncio.create_task(sending_data())

@ -1,12 +1,16 @@
import os
import io
import csv
import time
import itertools
import requests
import numpy as np
from datetime import datetime
_telemetry_error_last_ts = 0.0
def pack_elems(names, file_types, *elems):
if len(names) != len(file_types) or len(names) != len(elems):
raise ValueError('Длин массивов имен и типов файлов и не совпадает с количество элементов для сохранения')
@ -49,7 +53,6 @@ def send_data(data, localhost, localport, endpoint):
if response.status_code == 404 and fallback_port and str(localport) != str(fallback_port):
response_fb, url_fb = _post(fallback_port)
if response_fb.status_code == 200:
#print("Данные успешно отправлены и приняты!", url_fb)
return
print("Ошибка при отправке данных:", response_fb.status_code, url_fb)
return
@ -59,6 +62,37 @@ def send_data(data, localhost, localport, endpoint):
print(str(e))
def send_telemetry(data, host, port, endpoint='telemetry', timeout_sec=0.30):
"""
Best-effort отправка телеметрии на отдельный telemetry-server.
Ошибки намеренно не пробрасываются, чтобы не влиять на основной детект/аларм поток.
"""
global _telemetry_error_last_ts
host = '' if host is None else str(host).strip()
port = '' if port is None else str(port).strip()
endpoint = str(endpoint or 'telemetry').strip().lstrip('/')
if not host or not port:
return
try:
url = f"http://{host}:{port}/{endpoint}"
response = requests.post(url, json=data, timeout=float(timeout_sec))
if response.status_code == 200:
return
now = time.time()
if now - _telemetry_error_last_ts >= 10.0:
print(f"telemetry http error: {response.status_code} {url}")
_telemetry_error_last_ts = now
except Exception as exc:
now = time.time()
if now - _telemetry_error_last_ts >= 10.0:
print(f"telemetry send failed: {exc}")
_telemetry_error_last_ts = now
def save_data(path_to_save, freq, *args):
"""
Сохранение данных в csv файл. Используется для сохранения метрик и медиан сигнала на каналах с датой и временем

@ -0,0 +1,47 @@
import os
import time
from pathlib import Path
_DEFAULT_FLAG_PATH = Path(__file__).resolve().parents[2] / "runtime" / "jammer_active.flag"
_FLAG_PATH = Path(os.getenv("JAMMER_STATE_FILE", str(_DEFAULT_FLAG_PATH)))
_CACHE_TTL_SEC = float(os.getenv("JAMMER_STATE_CACHE_TTL_SEC", "0.25"))
_STALE_SEC = float(os.getenv("JAMMER_STATE_STALE_SEC", "5.0"))
_cached_value = False
_cached_checked_monotonic = 0.0
def _read_uncached() -> bool:
try:
stat = _FLAG_PATH.stat()
if time.time() - stat.st_mtime > _STALE_SEC:
return False
return _FLAG_PATH.read_text(encoding="ascii").strip() == "1"
except OSError:
return False
def is_jammer_active() -> bool:
global _cached_value
global _cached_checked_monotonic
now = time.monotonic()
if now - _cached_checked_monotonic < _CACHE_TTL_SEC:
return _cached_value
_cached_value = _read_uncached()
_cached_checked_monotonic = now
return _cached_value
def set_jammer_active(active: bool) -> None:
global _cached_value
global _cached_checked_monotonic
_FLAG_PATH.parent.mkdir(parents=True, exist_ok=True)
tmp_path = _FLAG_PATH.with_name(f"{_FLAG_PATH.name}.{os.getpid()}.tmp")
tmp_path.write_text("1" if active else "0", encoding="ascii")
os.replace(tmp_path, _FLAG_PATH)
_cached_value = bool(active)
_cached_checked_monotonic = time.monotonic()

@ -36,6 +36,9 @@ class TelemetryPoint(BaseModel):
alarm: bool = False
channel_idx: int = 0
channels_total: int = 1
channel_values: Optional[List[float]] = None
channel_thresholds: Optional[List[Optional[float]]] = None
alarm_channels: Optional[List[int]] = None
def _prune_freq_locked(freq: str, now_ts: float) -> None:
@ -136,15 +139,25 @@ MONITOR_HTML = """
--text: #1c232e;
--green: #12b76a;
--red: #ef4444;
--muted: #5b6574;
}
body { margin: 0; background: var(--bg); color: var(--text); font-family: system-ui, -apple-system, Segoe UI, sans-serif; }
.wrap { max-width: 1400px; margin: 0 auto; padding: 16px; }
.head { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; }
.meta { font-size: 13px; color: #5b6574; }
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(420px, 1fr)); gap: 12px; }
.card { background: var(--card); border: 1px solid var(--line); border-radius: 10px; padding: 8px 8px 2px; }
.title { font-size: 14px; font-weight: 600; margin: 6px 8px; }
.plot { height: 280px; }
.wrap { max-width: 1800px; margin: 0 auto; padding: 14px; }
.head { display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; }
.meta { font-size: 13px; color: var(--muted); }
.grid { display: flex; flex-direction: column; gap: 10px; }
.card { width: 100%; background: var(--card); border: 1px solid var(--line); border-radius: 10px; padding: 8px 8px 8px; }
.title-row { display: flex; justify-content: space-between; align-items: center; margin: 4px 8px; }
.title { font-size: 20px; font-weight: 700; }
.ctrl { display: flex; align-items: center; gap: 6px; }
.ctrl label { font-size: 12px; color: var(--muted); }
.ctrl select { border: 1px solid var(--line); border-radius: 6px; padding: 2px 6px; }
.plot { height: 260px; width: 100%; }
.events-title { font-size: 12px; color: var(--muted); margin: 2px 8px 4px; }
.events { max-height: 110px; overflow-y: auto; border-top: 1px dashed var(--line); margin: 0 8px; padding-top: 4px; }
.ev { display: flex; justify-content: space-between; font-size: 12px; line-height: 1.4; color: var(--text); }
.ev-t { color: var(--muted); }
.ev-empty { color: var(--muted); font-size: 12px; }
</style>
</head>
<body>
@ -152,7 +165,7 @@ MONITOR_HTML = """
<div class=\"head\">
<div>
<h2 style=\"margin:0;\">DroneDetector Telemetry Monitor</h2>
<div class=\"meta\">Green: dBFS current, Red: dynamic alarm threshold</div>
<div class=\"meta\">Green: dBFS current, Red: channel threshold, Red dots: alarm points</div>
</div>
<div class=\"meta\" id=\"status\">connecting...</div>
</div>
@ -162,17 +175,77 @@ MONITOR_HTML = """
<script>
const windowSec = 300;
const state = {}; // freq -> points[]
const selectedChannel = {}; // freq -> 'max' | channel index as string
function numericSortFreq(a, b) {
return Number(a) - Number(b);
}
function formatTime(ts) {
return new Date(Number(ts) * 1000).toLocaleTimeString('ru-RU', {hour12: false});
}
function getChannelCount(freq) {
const pts = state[freq] || [];
let maxCount = 1;
for (const p of pts) {
if (Number.isFinite(Number(p.channels_total))) {
maxCount = Math.max(maxCount, Number(p.channels_total));
}
if (Array.isArray(p.channel_values)) {
maxCount = Math.max(maxCount, p.channel_values.length);
}
}
return maxCount;
}
function ensurePlot(freq) {
if (document.getElementById(`plot-${freq}`)) return;
const card = document.createElement('div');
card.className = 'card';
card.innerHTML = `<div class=\"title\">${freq} MHz</div><div class=\"plot\" id=\"plot-${freq}\"></div>`;
card.innerHTML = `
<div class=\"title-row\">
<div class=\"title\">${freq} MHz</div>
<div class=\"ctrl\">
<label for=\"chan-${freq}\">channel</label>
<select id=\"chan-${freq}\"></select>
</div>
</div>
<div class=\"plot\" id=\"plot-${freq}\"></div>
<div class=\"events-title\">Alarms (time -> channel)</div>
<div class=\"events\" id=\"events-${freq}\"></div>
`;
document.getElementById('plots').appendChild(card);
selectedChannel[freq] = 'max';
const sel = document.getElementById(`chan-${freq}`);
sel.addEventListener('change', () => {
selectedChannel[freq] = sel.value;
render(freq);
});
}
function updateChannelSelector(freq) {
const sel = document.getElementById(`chan-${freq}`);
if (!sel) return;
const prev = selectedChannel[freq] ?? 'max';
const count = getChannelCount(freq);
const opts = ['max'];
for (let i = 0; i < count; i += 1) opts.push(String(i));
sel.innerHTML = '';
for (const v of opts) {
const option = document.createElement('option');
option.value = v;
option.textContent = v === 'max' ? 'max' : `ch ${v}`;
sel.appendChild(option);
}
selectedChannel[freq] = opts.includes(prev) ? prev : 'max';
sel.value = selectedChannel[freq];
}
function trimPoints(freq) {
@ -181,47 +254,134 @@ function trimPoints(freq) {
state[freq] = arr.filter(p => Number(p.ts) >= cutoff);
}
function getPointValueForSelection(point, selection) {
if (selection === 'max') {
return {
y: point.dbfs_current ?? null,
threshold: point.dbfs_threshold ?? null,
};
}
const idx = Number(selection);
if (!Number.isInteger(idx)) {
return {y: null, threshold: null};
}
const y = Array.isArray(point.channel_values) && idx < point.channel_values.length
? point.channel_values[idx]
: null;
const threshold = Array.isArray(point.channel_thresholds) && idx < point.channel_thresholds.length
? point.channel_thresholds[idx]
: null;
return {y, threshold};
}
function isAlarmForSelection(point, selection) {
if (point.alarm !== true) return false;
if (selection === 'max') return true;
const idx = Number(selection);
if (!Number.isInteger(idx)) return false;
if (Array.isArray(point.alarm_channels) && point.alarm_channels.length > 0) {
return point.alarm_channels.includes(idx);
}
return Number(point.channel_idx) === idx;
}
function renderAlarmEvents(freq, pts) {
const el = document.getElementById(`events-${freq}`);
if (!el) return;
const alarmPts = pts.filter(p => p.alarm === true);
if (alarmPts.length === 0) {
el.innerHTML = '<div class=\"ev-empty\">no alarms</div>';
return;
}
const rows = alarmPts.slice(-20).reverse().map((p) => {
const channels = Array.isArray(p.alarm_channels) && p.alarm_channels.length > 0
? p.alarm_channels.join(',')
: String(p.channel_idx ?? '-');
return `<div class=\"ev\"><span class=\"ev-t\">${formatTime(p.ts)}</span><span>ch ${channels}</span></div>`;
});
el.innerHTML = rows.join('');
}
function render(freq) {
ensurePlot(freq);
trimPoints(freq);
updateChannelSelector(freq);
const pts = state[freq] || [];
const sel = selectedChannel[freq] ?? 'max';
const x = [];
const y = [];
const thr = [];
const alarmX = [];
const alarmY = [];
for (const p of pts) {
const metric = getPointValueForSelection(p, sel);
if (metric.y === null || metric.y === undefined) {
continue;
}
const x = pts.map(p => new Date(Number(p.ts) * 1000));
const y = pts.map(p => p.dbfs_current);
const thr = pts.map(p => p.dbfs_threshold);
const alarmPts = pts.filter(p => p.alarm === true);
const ts = new Date(Number(p.ts) * 1000);
x.push(ts);
y.push(metric.y);
thr.push(metric.threshold);
if (isAlarmForSelection(p, sel)) {
alarmX.push(ts);
alarmY.push(metric.y);
}
}
const labelSuffix = sel === 'max' ? 'max' : `ch ${sel}`;
const traces = [
{
x,
y,
mode: 'lines',
name: 'dBFS',
line: {color: '#12b76a', width: 2}
name: `dBFS (${labelSuffix})`,
line: {color: '#12b76a', width: 2},
},
{
x,
y: thr,
mode: 'lines',
name: 'Threshold',
line: {color: '#ef4444', width: 2, dash: 'dash'}
name: `Threshold (${labelSuffix})`,
line: {color: '#ef4444', width: 2, dash: 'dash'},
},
{
x: alarmPts.map(p => new Date(Number(p.ts) * 1000)),
y: alarmPts.map(p => p.dbfs_current),
x: alarmX,
y: alarmY,
mode: 'markers',
name: 'Alarm',
marker: {color: '#ef4444', size: 6, symbol: 'circle'}
}
marker: {color: '#ef4444', size: 6, symbol: 'circle'},
},
];
Plotly.react(`plot-${freq}`, traces, {
margin: {l: 40, r: 12, t: 12, b: 32},
showlegend: true,
legend: {orientation: 'h', y: 1.16},
xaxis: {title: 'time'},
yaxis: {title: 'dBFS'}
xaxis: {
title: 'time',
tickformat: '%H:%M:%S',
hoverformat: '%H:%M:%S',
range: [new Date(Date.now() - windowSec * 1000), new Date()],
},
yaxis: {title: 'dBFS'},
}, {displayModeBar: false, responsive: true});
renderAlarmEvents(freq, pts);
}
function renderAll() {
@ -259,7 +419,9 @@ function connectWs() {
renderAll();
return;
}
if (msg.type !== 'point') return;
const p = msg.data;
const freq = String(p.freq);
if (!state[freq]) state[freq] = [];
@ -277,6 +439,10 @@ function connectWs() {
};
}
setInterval(() => {
renderAll();
}, 1000);
loadInitial().then(connectWs).catch((e) => {
document.getElementById('status').textContent = `init error: ${e}`;
connectWs();

Loading…
Cancel
Save