Compare commits

..

14 Commits
main ... fft

1
.gitignore vendored

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

@ -148,19 +148,7 @@ docker compose -f deploy/docker/docker-compose.yml down
``` ```
### Все SDR-сервисы (systemd) ### Все SDR-сервисы (systemd)
Запуск всех SDR unitов:
```bash
sudo systemctl start dronedetector-sdr-433.service
sudo systemctl start dronedetector-sdr-750.service
sudo systemctl start dronedetector-sdr-868.service
sudo systemctl start dronedetector-sdr-3300.service
sudo systemctl start dronedetector-sdr-4500.service
sudo systemctl start dronedetector-sdr-5200.service
sudo systemctl start dronedetector-sdr-5800.service
sudo systemctl start dronedetector-sdr-915.service
sudo systemctl start dronedetector-sdr-1200.service
sudo systemctl start dronedetector-sdr-2400.service
```
```bash ```bash
sudo systemctl status dronedetector-sdr-433.service sudo systemctl status dronedetector-sdr-433.service
sudo systemctl status dronedetector-sdr-750.service sudo systemctl status dronedetector-sdr-750.service
@ -188,26 +176,13 @@ sudo systemctl stop dronedetector-sdr-2400.service
``` ```
Альтернатива одной командой:
```bash
for u in dronedetector-sdr-{433,750,868,3300,4500,5200,5800,915,1200,2400}.service; do
sudo systemctl start "$u"
done
```
Проверка статуса всех SDR unitов: Проверка статуса всех SDR unitов:
```bash ```bash
sudo systemctl status dronedetector-sdr-*.service --no-pager sudo systemctl status dronedetector-sdr-*.service --no-pager
``` ```
### Полный ручной старт всего контура
```bash
docker compose -f deploy/docker/docker-compose.yml up -d
for u in dronedetector-sdr-{433,750,868,3300,4500,5200,5800,915,1200,2400}.service; do
sudo systemctl start "$u"
done
```
### Просмотр логов SDR ### Просмотр логов SDR
``` bash ``` bash
# 50 последних # 50 последних

@ -0,0 +1,18 @@
# syntax=docker/dockerfile:1.7
FROM python:3.11-slim
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
PYTHONPATH=/app
WORKDIR /app
COPY deploy/requirements/telemetry_server.txt /tmp/requirements.txt
RUN --mount=type=cache,target=/root/.cache/pip \
pip install --upgrade pip && \
pip install -r /tmp/requirements.txt
COPY . /app
EXPOSE 5020
CMD ["python3", "-m", "telemetry.telemetry_server"]

@ -9,6 +9,7 @@ services:
- ../../.env - ../../.env
environment: environment:
- PYTHONPATH=/app - PYTHONPATH=/app
- JAMMER_STATE_FILE=/app/runtime/jammer_active.flag
working_dir: /app working_dir: /app
command: ["python3", "-m", "src.server_to_master"] command: ["python3", "-m", "src.server_to_master"]
restart: unless-stopped restart: unless-stopped
@ -16,6 +17,7 @@ services:
- "5010:5010" - "5010:5010"
volumes: volumes:
- ../../.env:/app/.env:ro - ../../.env:/app/.env:ro
- ../../runtime:/app/runtime
networks: networks:
- dronedetector-net - dronedetector-net
@ -48,6 +50,29 @@ services:
networks: networks:
- dronedetector-net - dronedetector-net
dronedetector-telemetry-server:
container_name: dronedetector-telemetry-server
image: dronedetector-telemetry-server:latest
build:
context: ../..
dockerfile: deploy/docker/Dockerfile.telemetry_server
env_file:
- ../../.env
environment:
- PYTHONPATH=/app
working_dir: /app
command: ["python3", "-m", "telemetry.telemetry_server"]
restart: unless-stopped
ports:
- "5020:5020"
volumes:
- ../../.env:/app/.env:ro
- ../../telemetry:/app/telemetry
- ../../common:/app/common
networks:
- dronedetector-net
networks: networks:
dronedetector-net: dronedetector-net:
name: dronedetector-net name: dronedetector-net

@ -0,0 +1,3 @@
fastapi==0.115.6
uvicorn[standard]==0.32.1
python-dotenv==1.0.1

@ -12,7 +12,6 @@ from common.runtime import load_root_env, resolve_hackrf_index
load_root_env(__file__) load_root_env(__file__)
def get_hack_id(): def get_hack_id():
return resolve_hackrf_index('HACKID_1200', 'orange_scripts/main_1200.py') return resolve_hackrf_index('HACKID_1200', 'orange_scripts/main_1200.py')
serial_number = os.getenv('HACKID_1200') serial_number = os.getenv('HACKID_1200')

@ -12,7 +12,6 @@ from common.runtime import load_root_env, resolve_hackrf_index
load_root_env(__file__) load_root_env(__file__)
def get_hack_id(): def get_hack_id():
return resolve_hackrf_index('HACKID_2400', 'orange_scripts/main_2400.py') return resolve_hackrf_index('HACKID_2400', 'orange_scripts/main_2400.py')
serial_number = os.getenv('HACKID_2400') serial_number = os.getenv('HACKID_2400')

@ -12,7 +12,6 @@ from common.runtime import load_root_env, resolve_hackrf_index
load_root_env(__file__) load_root_env(__file__)
def get_hack_id(): def get_hack_id():
return resolve_hackrf_index('HACKID_915', 'orange_scripts/main_915.py') return resolve_hackrf_index('HACKID_915', 'orange_scripts/main_915.py')
serial_number = os.getenv('HACKID_915') serial_number = os.getenv('HACKID_915')

BIN
out.iq

Binary file not shown.

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

@ -1,7 +1,10 @@
import os
import statistics import statistics
from datetime import datetime
# Более лучшая версия кода есть в FRScanner # Более лучшая версия кода есть в FRScanner
class DataBuffer: class DataBuffer:
""" """
Класс с реализацией циклического буффера. Класс с реализацией циклического буффера.
@ -9,29 +12,20 @@ class DataBuffer:
Атрибуты: Атрибуты:
current_column: Указатель на текущий столбец буфера, который обновляем. current_column: Указатель на текущий столбец буфера, который обновляем.
thinning_counter: Прореживающий множитель на текующей итерации. thinning_counter: Прореживающий множитель на текующей итерации.
current_counter: Указатель на количество чтений между последним обновлением столбца и предыдущим атрибутом. current_counter: Указатель на количество чтений между последним обновлением столбца и предыдущим атрибутом.
num_of_thinning_iter: Прореживающий множитель. Раз в это количечество раз будет обнволяться столбец буфера. num_of_thinning_iter: Прореживающий множитель. Раз в это количество раз будет обновляться столбец буфера.
line_size: Количество строк буфера = количеству каналов. line_size: Количество строк буфера = количеству каналов.
columns_size: Количество столбцов = фиксированное число. columns_size: Количество столбцов = фиксированное число.
multiply_factor: Процентный показатель превышения сигналом уровня шума. ex m_p = 1.1 => триггер, если multiply_factor: Процентный показатель превышения сигналом уровня шума (legacy).
сигнал превышает шум на 10%. num_for_alarm: Количество раз, превышающих шум, при которых триггеримся.
num_for_alarm: Количество раз, превышающих шум, при которых триггеримся = фиксированное число. is_init: Флаг инициализации буфера.
is_init: Флаг инициализации буфера. = True, если инициализирован.
buffer: Массив для буфера. buffer: Массив для буфера.
buffer_medians: Массив для медиан столбцов букера. buffer_medians: Массив медиан по каналам.
buffer_alarms: Массив для количества тревог по столбца буфера. buffer_mads: Массив MAD по каналам.
buffer_alarms: Массив для количества тревог по каналам.
""" """
def __init__(self, columns_size, num_of_thinning_iter, num_of_channels, multiply_factor, num_for_alarm): def __init__(self, columns_size, num_of_thinning_iter, num_of_channels, multiply_factor, num_for_alarm, freq_tag=None):
"""
Инициализируем класс.
:param columns_size:
:param num_of_thinning_iter:
:param num_of_channels:
:param multiply_factor:
:param num_for_alarm:
"""
self.current_column = 0 self.current_column = 0
self.thinning_counter = 1 self.thinning_counter = 1
self.current_counter = 1 self.current_counter = 1
@ -41,19 +35,46 @@ class DataBuffer:
self.multiply_factor = multiply_factor self.multiply_factor = multiply_factor
self.num_for_alarm = num_for_alarm self.num_for_alarm = num_for_alarm
self.is_init = False self.is_init = False
self.buffer = [[0 for _ in range(self.columns_size)] for _ in range(self.line_size)] 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.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.freq_tag = '' if freq_tag is None else str(freq_tag)
suffix = f'_{self.freq_tag}' if self.freq_tag else ''
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): def get_buffer(self):
return self.buffer return self.buffer
def get_timestamps(self):
return self.buffer_timestamps
def get_medians(self): def get_medians(self):
return self.buffer_medians return self.buffer_medians
def get_mads(self):
return self.buffer_mads
def get_alarms(self): def get_alarms(self):
return self.buffer_alarms return self.buffer_alarms
def get_last_alarm_channels(self):
return list(self.last_alarm_channels)
def check_init(self): def check_init(self):
return self.is_init return self.is_init
@ -63,20 +84,89 @@ class DataBuffer:
print(self.buffer[i], end=' ') print(self.buffer[i], end=' ')
print() 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): def medians(self):
""" """
Вычислить медиану по строке буфера. Вычислить медиану и MAD по строкам буфера.
:return: None :return: None
""" """
if self.check_init(): if self.check_init():
for i in range(self.line_size): for i in range(self.line_size):
self.buffer_medians[i] = statistics.median(self.buffer[i]) med = float(statistics.median(self.buffer[i]))
# print('medians is: ', self.buffer_medians) self.buffer_medians[i] = med
# return self.buffer_medians 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): def alarms_fill_zeros(self):
self.buffer_alarms = [0] * self.line_size self.buffer_alarms = [0] * self.line_size
def update(self, data): self.last_alarm_channels = []
def update(self, data, packet_timestamps=None):
""" """
Обновление буфера. Обновление буфера.
Если номер текущего чтения совпадает с количеством прореживающего множителя на текущем обновлении буфера, то Если номер текущего чтения совпадает с количеством прореживающего множителя на текущем обновлении буфера, то
@ -85,86 +175,84 @@ class DataBuffer:
3. Берем медианы по буферу, если он уже проиницализирован. 3. Берем медианы по буферу, если он уже проиницализирован.
4. Сбрасываем счетчик текущих чтений. 4. Сбрасываем счетчик текущих чтений.
5. Если был последний столбец (и мы уже переключились на первый), то 5. Если был последний столбец (и мы уже переключились на первый), то
Если прореживающий множитель на текующей итерации был единица, то мы иницилизировались Если прореживающий множитель на текующей итерации был единица, то мы иницилизировались.
До тех пор, пока множитель на итерации меньше фиксированного, увеличиваем в два раза. До тех пор, пока множитель на итерации меньше фиксированного, увеличиваем в два раза.
В противном случае - увеличиваем номер чтения. В противном случае - увеличиваем номер чтения.
:param data: Массив с метриками сигнала по каналам. :param data: Массив с метриками сигнала по каналам.
:param packet_timestamps: Времена пакетов SDR для каждой метрики канала.
:return: None :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')
# TODO: Добавить время релаксации - если система затриггерилась, то перестать обновлять буфер на N чтений, sample_idx = self.current_counter - 1
# где N задается в .env-template. Сейчас есть бага, что буфер перестает обновляться только когда система for i in range(self.line_size):
# триггерится. Между тем, когда приходит аларм и num_for_alarm, когда сигнал алармовский, но система еще не self.episode_history[i][sample_idx] = float(data[i])
# триггерится, буфер продолжает обновляться. В таких условиях буфер может набрать в себя алармовских сигналов self.episode_history_timestamps[i][sample_idx] = packet_timestamps[i]
# и повысить пороги. Пример такой ситуации: дрон висит на 1км, система его видит, но сигнал превышает порог раз
# через раз и аларм срабатывает не всегда. В таких условиях наберется высокий сигнал, повысятся пороги и когда
# дрон начнет движение вперед, он будет заметен на более низкой дистанции, чем обычно, так как пороги повышены.
if self.current_counter == self.thinning_counter: if self.current_counter == self.thinning_counter:
updated_column = self.current_column
for i in range(self.line_size): 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.current_column = (self.current_column + 1) % self.columns_size
#print('Столбец {0} обновлен. Перешли к столбцу {1}: '.format(self.current_column - 1, self.current_column))
self.medians() 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 self.current_counter = 1
if self.current_column == 0: if self.current_column == 0:
if self.thinning_counter == 1: if self.thinning_counter == 1:
self.is_init = True self.is_init = True
self.medians() self.medians()
print('Начальная калибровка завершена.') print('Начальная калибровка завершена.')
self.log_threshold_update(updated_column)
if self.thinning_counter < self.num_of_thinning_iter: if self.thinning_counter < self.num_of_thinning_iter:
self.thinning_counter *= 2 self.thinning_counter *= 2
# print('thinning counter обновлен: ', self.thinning_counter)
else: else:
self.current_counter += 1 self.current_counter += 1
# print('curr counter обновлен: ', self.current_counter)
def check_alarm(self, data): def check_alarm(self, data):
""" """
Проверка триггера системы. Проверка триггера системы по dBFS во времени.
Если значение по каналу превышает медиану (порог) шума на какой-то процент, то инкремент буфер аларма по каналу. Один порог на канал, набор тревоги и сброс счетчиков как в main.
Превышение num_for_alarm подряд - триггер. Если после n превышений, где n<num_for_alarm приходит сигнал не
первышающий порог, то сбрасываем буфер алармов.
:param data:
:return: Да/нет.
""" """
if self.check_init(): if self.check_init():
ratios=[] self.last_alarm_channels = []
print("="*50)
for i in range(len(data)): for i in range(len(data)):
exceeding = data[i] > self.multiply_factor * self.buffer_medians[i] current = data[i]
ratios.append(data[i]/self.buffer_medians[i]) threshold = self.get_threshold(i)
exceeding = current >= threshold
if exceeding: if exceeding:
self.buffer_alarms[i] += 1 self.buffer_alarms[i] += 1
# print('Инкремент буффер алармов по каналу {0}, текущее число по этому каналу: {1}'.format(i,self.buffer_alarms[i]))
else: else:
self.buffer_alarms[i] = 0 self.buffer_alarms[i] = 0
# print('Обнулили буффер алармов по каналу {0}, текущее число по этому каналу: {1}'.format(i,self.buffer_alarms[i]))
if self.buffer_alarms[i] >= self.num_for_alarm: if self.buffer_alarms[i] >= self.num_for_alarm:
# print('Сработала тревога по каналу {0}, текущее число по этому каналу: {1}'.format(i,self.buffer_alarms[i])) self.last_alarm_channels = [i]
self.buffer_alarms = [0] * self.line_size self.buffer_alarms = [0] * self.line_size
print("Отношения:", [f"{r:.3f}" for r in ratios])
print("!"*50)
return True return True
print("Отношения:", [f"{r:.3f}" for r in ratios])
print("="*50)
return False return False
def check_single_alarm(self, median, cur_channel): def check_single_alarm(self, median, cur_channel):
""" """
Проверка, является ли текущая медиана по каналу превышающей порог. Проверка, является ли текущая метрика по каналу превышающей порог.
:param median: меди (хар-ка) по каналу. :param median: текущая метрика в dBFS.
:param cur_channel: индекс канала внутри частоты. :param cur_channel: индекс канала внутри частоты.
:return: Да/нет. :return: Да/нет.
""" """
if self.check_init(): if self.check_init():
exceeding = median > self.multiply_factor * self.buffer_medians[cur_channel] threshold = self.get_threshold(cur_channel)
print(median/self.buffer_medians[cur_channel]) return median >= threshold
if exceeding:
return True return False
else:
return False

@ -155,7 +155,15 @@ class MultiChannel:
num_for_alarm = int(os.getenv('num_for_alarm_' + str(freq))) num_for_alarm = int(os.getenv('num_for_alarm_' + str(freq)))
num_chs = self.get_num_chs(i) num_chs = self.get_num_chs(i)
self.DB.append( 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): def db_alarms_zeros(self, circle_buffer):
""" """

@ -1,4 +1,6 @@
import os import os
import math
import time
import numpy as np import numpy as np
from typing import Union from typing import Union
from common.runtime import load_root_env from common.runtime import load_root_env
@ -24,6 +26,10 @@ class Signal:
self.conv_method = conv_method self.conv_method = conv_method
self.signal = [] self.signal = []
self.signal_abs = [] 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): def get_signal(self):
""" """
@ -32,6 +38,9 @@ class Signal:
""" """
return self.signal, self.signal_abs return self.signal, self.signal_abs
def get_last_packet_ts(self):
return self.last_packet_ts
def clear(self) -> None: def clear(self) -> None:
""" """
Очистить массив с сигналом после предобработки? Очистить массив с сигналом после предобработки?
@ -39,6 +48,31 @@ class Signal:
""" """
self.signal = [] self.signal = []
self.signal_abs = [] 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: def signal_preprocessing(self, length) -> float:
""" """
@ -46,15 +80,33 @@ class Signal:
:return: Число типа float - "характеристика сигнала". :return: Число типа float - "характеристика сигнала".
""" """
signal = np.array([self.signal.real[0:length], self.signal.imag[0:length]], dtype=np.float32) samples = np.asarray(self.signal).ravel()[0:length]
signal_abs = np.linalg.norm(signal, axis=0) # Поэлементный модуль комплексного числа. shape.result if samples.size == 0:
# (1, self.length) 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': if self.conv_method == 'max':
result = np.max(signal_abs) result = float(np.max(scalar_samples))
else: else:
result = np.median(signal_abs) result = float(np.median(scalar_samples))
self.signal = signal
self.signal_abs = signal_abs self.signal = scalar_samples
self.signal_abs = np.abs(scalar_samples)
return result return result
def fill_signal(self, lvl, length) -> Union[int, float]: def fill_signal(self, lvl, length) -> Union[int, float]:
@ -69,8 +121,8 @@ class Signal:
self.signal = np.concatenate((self.signal, y), axis=None) self.signal = np.concatenate((self.signal, y), axis=None)
return 0 return 0
else: else:
self.last_packet_ts = time.time()
preproc_signal = self.signal_preprocessing(length) preproc_signal = self.signal_preprocessing(length)
#self.clear()
return preproc_signal return preproc_signal
@ -81,27 +133,33 @@ class SignalsArray:
sig_array: Список для сохранения медиан. sig_array: Список для сохранения медиан.
counter: Индикатор наполненности массива. counter: Индикатор наполненности массива.
""" """
def __init__(self): def __init__(self):
self.sig_array = [] self.sig_array = []
self.sig_ts_array = []
self.counter = 0 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. Аппендим характеристику сигнала (метрику) в массив длиной num_chs.
:param metrica: Характеристика сигнала (метрика). :param metrica: Характеристика сигнала (метрика).
:param packet_ts: Время завершения набора пакета с SDR для текущей метрики.
:param num_chs: Количество каналов на частоте. :param num_chs: Количество каналов на частоте.
:return: Индекс канала внутри частоты и массив с характеристиками, если заполнен, иначе - пустой. :return: Индекс канала внутри частоты и массив с характеристиками, если заполнен, иначе - пустой.
""" """
if num_chs: if num_chs:
if self.counter < num_chs: if self.counter < num_chs:
self.sig_array.append(metrica) self.sig_array.append(metrica)
self.sig_ts_array.append(packet_ts)
self.counter += 1 self.counter += 1
if self.counter == num_chs: if self.counter == num_chs:
arr = self.sig_array arr = self.sig_array
arr_ts = self.sig_ts_array
self.sig_array = [] self.sig_array = []
self.sig_ts_array = []
self.counter = 0 self.counter = 0
return num_chs - 1, arr return num_chs - 1, arr, arr_ts
else: else:
return self.counter - 1, [] return self.counter - 1, [], []
else: else:
return 0, [] return 0, [], []

@ -1,8 +1,10 @@
import os import os
import datetime import datetime
import time
from common.runtime import load_root_env, validate_env, as_bool, as_str from common.runtime import load_root_env, validate_env, as_bool, as_str
from smb.SMBConnection import SMBConnection 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.sig_n_medi_collect import Signal, SignalsArray, get_signal_length
from core.multichannelswitcher import MultiChannel, get_centre_freq 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') remote_pc_name = os.getenv('remote_pc_name')
smb_domain = os.getenv('smb_domain') smb_domain = os.getenv('smb_domain')
freq_endpoint = os.getenv('freq_endpoint') 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(',') elems_to_save = elems_to_save.split(',')
file_types_to_save = file_types_to_save.split(',') file_types_to_save = file_types_to_save.split(',')
@ -69,11 +76,12 @@ def work(lvl):
freq = get_centre_freq(f) freq = get_centre_freq(f)
signal_length = get_signal_length(freq) signal_length = get_signal_length(freq)
median = tmp_signal.fill_signal(lvl, signal_length) median = tmp_signal.fill_signal(lvl, signal_length)
packet_ts = tmp_signal.get_last_packet_ts()
if median: if median:
try: try:
num_chs, circle_buffer = multi_channel.check_f(f) 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: if sigs_array:
print('Значения на {0}: {1}'.format(freq, sigs_array)) print('Значения на {0}: {1}'.format(freq, sigs_array))
@ -83,8 +91,40 @@ def work(lvl):
if alarm: if alarm:
print('----ALARM---- ', freq) print('----ALARM---- ', freq)
multi_channel.db_alarms_zeros(circle_buffer) multi_channel.db_alarms_zeros(circle_buffer)
else: elif not is_jammer_active():
circle_buffer.update(sigs_array) 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: if send_to_module_flag:
send_data(agregator(freq, alarm), localhost, localport, freq_endpoint) send_data(agregator(freq, alarm), localhost, localport, freq_endpoint)

@ -1,8 +1,10 @@
import os import os
import datetime import datetime
import time
from common.runtime import load_root_env, validate_env, as_bool, as_str from common.runtime import load_root_env, validate_env, as_bool, as_str
from smb.SMBConnection import SMBConnection 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.sig_n_medi_collect import Signal, SignalsArray, get_signal_length
from core.multichannelswitcher import MultiChannel, get_centre_freq 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') remote_pc_name = os.getenv('remote_pc_name')
smb_domain = os.getenv('smb_domain') smb_domain = os.getenv('smb_domain')
freq_endpoint = os.getenv('freq_endpoint') 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(',') elems_to_save = elems_to_save.split(',')
file_types_to_save = file_types_to_save.split(',') file_types_to_save = file_types_to_save.split(',')
@ -69,11 +76,12 @@ def work(lvl):
freq = get_centre_freq(f) freq = get_centre_freq(f)
signal_length = get_signal_length(freq) signal_length = get_signal_length(freq)
median = tmp_signal.fill_signal(lvl, signal_length) median = tmp_signal.fill_signal(lvl, signal_length)
packet_ts = tmp_signal.get_last_packet_ts()
if median: if median:
try: try:
num_chs, circle_buffer = multi_channel.check_f(f) 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: if sigs_array:
print('Значения на {0}: {1}'.format(freq, sigs_array)) print('Значения на {0}: {1}'.format(freq, sigs_array))
@ -83,8 +91,40 @@ def work(lvl):
if alarm: if alarm:
print('----ALARM---- ', freq) print('----ALARM---- ', freq)
multi_channel.db_alarms_zeros(circle_buffer) multi_channel.db_alarms_zeros(circle_buffer)
else: elif not is_jammer_active():
circle_buffer.update(sigs_array) 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: if send_to_module_flag:
send_data(agregator(freq, alarm), localhost, localport, freq_endpoint) send_data(agregator(freq, alarm), localhost, localport, freq_endpoint)

@ -1,8 +1,10 @@
import os import os
import datetime import datetime
import time
from common.runtime import load_root_env, validate_env, as_bool, as_str from common.runtime import load_root_env, validate_env, as_bool, as_str
from smb.SMBConnection import SMBConnection 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.sig_n_medi_collect import Signal, SignalsArray, get_signal_length
from core.multichannelswitcher import MultiChannel, get_centre_freq 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') remote_pc_name = os.getenv('remote_pc_name')
smb_domain = os.getenv('smb_domain') smb_domain = os.getenv('smb_domain')
freq_endpoint = os.getenv('freq_endpoint') 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(',') elems_to_save = elems_to_save.split(',')
file_types_to_save = file_types_to_save.split(',') file_types_to_save = file_types_to_save.split(',')
@ -69,11 +76,12 @@ def work(lvl):
freq = get_centre_freq(f) freq = get_centre_freq(f)
signal_length = get_signal_length(freq) signal_length = get_signal_length(freq)
median = tmp_signal.fill_signal(lvl, signal_length) median = tmp_signal.fill_signal(lvl, signal_length)
packet_ts = tmp_signal.get_last_packet_ts()
if median: if median:
try: try:
num_chs, circle_buffer = multi_channel.check_f(f) 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: if sigs_array:
print('Значения на {0}: {1}'.format(freq, sigs_array)) print('Значения на {0}: {1}'.format(freq, sigs_array))
@ -83,8 +91,40 @@ def work(lvl):
if alarm: if alarm:
print('----ALARM---- ', freq) print('----ALARM---- ', freq)
multi_channel.db_alarms_zeros(circle_buffer) multi_channel.db_alarms_zeros(circle_buffer)
else: elif not is_jammer_active():
circle_buffer.update(sigs_array) 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: if send_to_module_flag:
send_data(agregator(freq, alarm), localhost, localport, freq_endpoint) send_data(agregator(freq, alarm), localhost, localport, freq_endpoint)

@ -1,8 +1,10 @@
import os import os
import datetime import datetime
import time
from common.runtime import load_root_env, validate_env, as_bool, as_str from common.runtime import load_root_env, validate_env, as_bool, as_str
from smb.SMBConnection import SMBConnection 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.sig_n_medi_collect import Signal, SignalsArray, get_signal_length
from core.multichannelswitcher import MultiChannel, get_centre_freq 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') remote_pc_name = os.getenv('remote_pc_name')
smb_domain = os.getenv('smb_domain') smb_domain = os.getenv('smb_domain')
freq_endpoint = os.getenv('freq_endpoint') 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(',') elems_to_save = elems_to_save.split(',')
file_types_to_save = file_types_to_save.split(',') file_types_to_save = file_types_to_save.split(',')
@ -69,11 +76,12 @@ def work(lvl):
freq = get_centre_freq(f) freq = get_centre_freq(f)
signal_length = get_signal_length(freq) signal_length = get_signal_length(freq)
median = tmp_signal.fill_signal(lvl, signal_length) median = tmp_signal.fill_signal(lvl, signal_length)
packet_ts = tmp_signal.get_last_packet_ts()
if median: if median:
try: try:
num_chs, circle_buffer = multi_channel.check_f(f) 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: if sigs_array:
print('Значения на {0}: {1}'.format(freq, sigs_array)) print('Значения на {0}: {1}'.format(freq, sigs_array))
@ -83,8 +91,40 @@ def work(lvl):
if alarm: if alarm:
print('----ALARM---- ', freq) print('----ALARM---- ', freq)
multi_channel.db_alarms_zeros(circle_buffer) multi_channel.db_alarms_zeros(circle_buffer)
else: elif not is_jammer_active():
circle_buffer.update(sigs_array) 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: if send_to_module_flag:
send_data(agregator(freq, alarm), localhost, localport, freq_endpoint) send_data(agregator(freq, alarm), localhost, localport, freq_endpoint)

@ -1,8 +1,10 @@
import os import os
import datetime import datetime
import time
from common.runtime import load_root_env, validate_env, as_bool, as_str from common.runtime import load_root_env, validate_env, as_bool, as_str
from smb.SMBConnection import SMBConnection 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.sig_n_medi_collect import Signal, SignalsArray, get_signal_length
from core.multichannelswitcher import MultiChannel, get_centre_freq 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') remote_pc_name = os.getenv('remote_pc_name')
smb_domain = os.getenv('smb_domain') smb_domain = os.getenv('smb_domain')
freq_endpoint = os.getenv('freq_endpoint') 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(',') elems_to_save = elems_to_save.split(',')
file_types_to_save = file_types_to_save.split(',') file_types_to_save = file_types_to_save.split(',')
@ -69,11 +76,12 @@ def work(lvl):
freq = get_centre_freq(f) freq = get_centre_freq(f)
signal_length = get_signal_length(freq) signal_length = get_signal_length(freq)
median = tmp_signal.fill_signal(lvl, signal_length) median = tmp_signal.fill_signal(lvl, signal_length)
packet_ts = tmp_signal.get_last_packet_ts()
if median: if median:
try: try:
num_chs, circle_buffer = multi_channel.check_f(f) 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: if sigs_array:
print('Значения на {0}: {1}'.format(freq, sigs_array)) print('Значения на {0}: {1}'.format(freq, sigs_array))
@ -83,8 +91,40 @@ def work(lvl):
if alarm: if alarm:
print('----ALARM---- ', freq) print('----ALARM---- ', freq)
multi_channel.db_alarms_zeros(circle_buffer) multi_channel.db_alarms_zeros(circle_buffer)
else: elif not is_jammer_active():
circle_buffer.update(sigs_array) 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: if send_to_module_flag:
send_data(agregator(freq, alarm), localhost, localport, freq_endpoint) send_data(agregator(freq, alarm), localhost, localport, freq_endpoint)

@ -1,8 +1,10 @@
import os import os
import datetime import datetime
import time
from common.runtime import load_root_env, validate_env, as_bool, as_str from common.runtime import load_root_env, validate_env, as_bool, as_str
from smb.SMBConnection import SMBConnection 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.sig_n_medi_collect import Signal, SignalsArray, get_signal_length
from core.multichannelswitcher import MultiChannel, get_centre_freq 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') remote_pc_name = os.getenv('remote_pc_name')
smb_domain = os.getenv('smb_domain') smb_domain = os.getenv('smb_domain')
freq_endpoint = os.getenv('freq_endpoint') 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(',') elems_to_save = elems_to_save.split(',')
file_types_to_save = file_types_to_save.split(',') file_types_to_save = file_types_to_save.split(',')
@ -69,13 +76,14 @@ def work(lvl):
freq = get_centre_freq(f) freq = get_centre_freq(f)
signal_length = get_signal_length(freq) signal_length = get_signal_length(freq)
median = tmp_signal.fill_signal(lvl, signal_length) median = tmp_signal.fill_signal(lvl, signal_length)
packet_ts = tmp_signal.get_last_packet_ts()
if median: if median:
print(1) print(1)
try: try:
num_chs, circle_buffer = multi_channel.check_f(f) num_chs, circle_buffer = multi_channel.check_f(f)
print(num_chs, circle_buffer) 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) print(3)
if sigs_array: if sigs_array:
@ -86,8 +94,40 @@ def work(lvl):
if alarm: if alarm:
print('----ALARM---- ', freq) print('----ALARM---- ', freq)
multi_channel.db_alarms_zeros(circle_buffer) multi_channel.db_alarms_zeros(circle_buffer)
else: elif not is_jammer_active():
circle_buffer.update(sigs_array) 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: if send_to_module_flag:
send_data(agregator(freq, alarm), localhost, localport, freq_endpoint) send_data(agregator(freq, alarm), localhost, localport, freq_endpoint)

@ -1,8 +1,10 @@
import os import os
import datetime import datetime
import time
from common.runtime import load_root_env, validate_env, as_bool, as_str from common.runtime import load_root_env, validate_env, as_bool, as_str
from smb.SMBConnection import SMBConnection 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.sig_n_medi_collect import Signal, SignalsArray, get_signal_length
from core.multichannelswitcher import MultiChannel, get_centre_freq 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') remote_pc_name = os.getenv('remote_pc_name')
smb_domain = os.getenv('smb_domain') smb_domain = os.getenv('smb_domain')
freq_endpoint = os.getenv('freq_endpoint') 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(',') elems_to_save = elems_to_save.split(',')
file_types_to_save = file_types_to_save.split(',') file_types_to_save = file_types_to_save.split(',')
@ -69,11 +76,12 @@ def work(lvl):
freq = get_centre_freq(f) freq = get_centre_freq(f)
signal_length = get_signal_length(freq) signal_length = get_signal_length(freq)
median = tmp_signal.fill_signal(lvl, signal_length) median = tmp_signal.fill_signal(lvl, signal_length)
packet_ts = tmp_signal.get_last_packet_ts()
if median: if median:
try: try:
num_chs, circle_buffer = multi_channel.check_f(f) 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: if sigs_array:
print('Значения на {0}: {1}'.format(freq, sigs_array)) print('Значения на {0}: {1}'.format(freq, sigs_array))
@ -83,8 +91,40 @@ def work(lvl):
if alarm: if alarm:
print('----ALARM---- ', freq) print('----ALARM---- ', freq)
multi_channel.db_alarms_zeros(circle_buffer) multi_channel.db_alarms_zeros(circle_buffer)
else: elif not is_jammer_active():
circle_buffer.update(sigs_array) 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: if send_to_module_flag:
send_data(agregator(freq, alarm), localhost, localport, freq_endpoint) send_data(agregator(freq, alarm), localhost, localport, freq_endpoint)

@ -23,7 +23,6 @@ from common.runtime import load_root_env, resolve_hackrf_index
load_root_env(__file__) load_root_env(__file__)
def get_hack_id(): def get_hack_id():
return resolve_hackrf_index('hack_3300', 'src/main_3300.py') return resolve_hackrf_index('hack_3300', 'src/main_3300.py')
serial_number = os.getenv('hack_3300') serial_number = os.getenv('hack_3300')

@ -23,7 +23,6 @@ from common.runtime import load_root_env, resolve_hackrf_index
load_root_env(__file__) load_root_env(__file__)
def get_hack_id(): def get_hack_id():
return resolve_hackrf_index('hack_433', 'src/main_433.py') return resolve_hackrf_index('hack_433', 'src/main_433.py')
serial_number = os.getenv('hack_433') serial_number = os.getenv('hack_433')

@ -23,7 +23,6 @@ from common.runtime import load_root_env, resolve_hackrf_index
load_root_env(__file__) load_root_env(__file__)
def get_hack_id(): def get_hack_id():
return resolve_hackrf_index('hack_4500', 'src/main_4500.py') return resolve_hackrf_index('hack_4500', 'src/main_4500.py')
serial_number = os.getenv('hack_4500') serial_number = os.getenv('hack_4500')

@ -18,12 +18,10 @@ import time
import threading import threading
import subprocess import subprocess
import os import os
from common.runtime import load_root_env, resolve_hackrf_index from common.runtime import load_root_env, resolve_hackrf_index
load_root_env(__file__) load_root_env(__file__)
def get_hack_id(): def get_hack_id():
return resolve_hackrf_index('hack_5200', 'src/main_5200.py') return resolve_hackrf_index('hack_5200', 'src/main_5200.py')
serial_number = os.getenv('hack_5200') serial_number = os.getenv('hack_5200')

@ -23,7 +23,6 @@ from common.runtime import load_root_env, resolve_hackrf_index
load_root_env(__file__) load_root_env(__file__)
def get_hack_id(): def get_hack_id():
return resolve_hackrf_index('hack_5800', 'src/main_5800.py') return resolve_hackrf_index('hack_5800', 'src/main_5800.py')
serial_number = os.getenv('hack_5800') serial_number = os.getenv('hack_5800')

@ -23,7 +23,6 @@ from common.runtime import load_root_env, resolve_hackrf_index
load_root_env(__file__) load_root_env(__file__)
def get_hack_id(): def get_hack_id():
return resolve_hackrf_index('hack_750', 'src/main_750.py') return resolve_hackrf_index('hack_750', 'src/main_750.py')
serial_number = os.getenv('hack_750') serial_number = os.getenv('hack_750')

@ -23,7 +23,6 @@ from common.runtime import load_root_env, resolve_hackrf_index
load_root_env(__file__) load_root_env(__file__)
def get_hack_id(): def get_hack_id():
return resolve_hackrf_index('hack_868', 'src/main_868.py') return resolve_hackrf_index('hack_868', 'src/main_868.py')
serial_number = os.getenv('hack_868') serial_number = os.getenv('hack_868')

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

@ -1,12 +1,16 @@
import os import os
import io import io
import csv import csv
import time
import itertools import itertools
import requests import requests
import numpy as np import numpy as np
from datetime import datetime from datetime import datetime
_telemetry_error_last_ts = 0.0
def pack_elems(names, file_types, *elems): def pack_elems(names, file_types, *elems):
if len(names) != len(file_types) or len(names) != len(elems): if len(names) != len(file_types) or len(names) != len(elems):
raise ValueError('Длин массивов имен и типов файлов и не совпадает с количество элементов для сохранения') 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): if response.status_code == 404 and fallback_port and str(localport) != str(fallback_port):
response_fb, url_fb = _post(fallback_port) response_fb, url_fb = _post(fallback_port)
if response_fb.status_code == 200: if response_fb.status_code == 200:
#print("Данные успешно отправлены и приняты!", url_fb)
return return
print("Ошибка при отправке данных:", response_fb.status_code, url_fb) print("Ошибка при отправке данных:", response_fb.status_code, url_fb)
return return
@ -59,6 +62,37 @@ def send_data(data, localhost, localport, endpoint):
print(str(e)) 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): def save_data(path_to_save, freq, *args):
""" """
Сохранение данных в csv файл. Используется для сохранения метрик и медиан сигнала на каналах с датой и временем Сохранение данных в 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()

@ -0,0 +1 @@
# telemetry package

@ -0,0 +1,465 @@
import asyncio
import os
import time
from collections import defaultdict, deque
from typing import Any, Deque, Dict, List, Optional
from fastapi import FastAPI, Query, WebSocket, WebSocketDisconnect
from fastapi.responses import HTMLResponse
from pydantic import BaseModel, Field
from common.runtime import load_root_env
load_root_env(__file__)
TELEMETRY_BIND_HOST = os.getenv('telemetry_bind_host', os.getenv('lochost', '0.0.0.0'))
TELEMETRY_BIND_PORT = int(os.getenv('telemetry_bind_port', os.getenv('telemetry_port', '5020')))
TELEMETRY_HISTORY_SEC = int(float(os.getenv('telemetry_history_sec', '900')))
TELEMETRY_MAX_POINTS_PER_FREQ = int(os.getenv('telemetry_max_points_per_freq', '5000'))
def _new_buffer() -> Deque[Dict[str, Any]]:
return deque(maxlen=TELEMETRY_MAX_POINTS_PER_FREQ)
app = FastAPI(title='DroneDetector Telemetry Server')
_buffers: Dict[str, Deque[Dict[str, Any]]] = defaultdict(_new_buffer)
_ws_clients: List[WebSocket] = []
_state_lock = asyncio.Lock()
class TelemetryPoint(BaseModel):
freq: str
ts: float = Field(default_factory=lambda: time.time())
dbfs_current: float
dbfs_threshold: Optional[float] = None
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:
cutoff = now_ts - TELEMETRY_HISTORY_SEC
buf = _buffers[freq]
while buf and float(buf[0].get('ts', 0.0)) < cutoff:
buf.popleft()
def _copy_series_locked(seconds: int, freq: Optional[str] = None) -> Dict[str, List[Dict[str, Any]]]:
now_ts = time.time()
cutoff = now_ts - seconds
if freq is not None:
data = [point for point in _buffers.get(freq, []) if float(point.get('ts', 0.0)) >= cutoff]
return {freq: data}
series: Dict[str, List[Dict[str, Any]]] = {}
for key, buf in _buffers.items():
series[key] = [point for point in buf if float(point.get('ts', 0.0)) >= cutoff]
return series
async def _broadcast(message: Dict[str, Any]) -> None:
dead: List[WebSocket] = []
for ws in list(_ws_clients):
try:
await ws.send_json(message)
except Exception:
dead.append(ws)
if dead:
async with _state_lock:
for ws in dead:
if ws in _ws_clients:
_ws_clients.remove(ws)
@app.post('/telemetry')
async def ingest_telemetry(point: TelemetryPoint):
payload = point.model_dump()
freq = str(payload['freq'])
now_ts = time.time()
async with _state_lock:
_buffers[freq].append(payload)
_prune_freq_locked(freq, now_ts)
await _broadcast({'type': 'point', 'data': payload})
return {'ok': True}
@app.get('/telemetry/history')
async def telemetry_history(
freq: Optional[str] = Query(default=None),
seconds: int = Query(default=300, ge=10, le=86400),
):
seconds = min(seconds, TELEMETRY_HISTORY_SEC)
async with _state_lock:
series = _copy_series_locked(seconds=seconds, freq=freq)
return {'seconds': seconds, 'series': series}
@app.websocket('/telemetry/ws')
async def telemetry_ws(websocket: WebSocket):
await websocket.accept()
async with _state_lock:
_ws_clients.append(websocket)
snapshot = _copy_series_locked(seconds=min(300, TELEMETRY_HISTORY_SEC), freq=None)
await websocket.send_json({'type': 'snapshot', 'data': snapshot})
try:
while True:
# Keepalive channel from browser; content is ignored.
await websocket.receive_text()
except WebSocketDisconnect:
pass
finally:
async with _state_lock:
if websocket in _ws_clients:
_ws_clients.remove(websocket)
MONITOR_HTML = """
<!doctype html>
<html>
<head>
<meta charset=\"utf-8\" />
<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />
<title>DroneDetector Telemetry</title>
<script src=\"https://cdn.plot.ly/plotly-2.35.2.min.js\"></script>
<style>
:root {
--bg: #f6f8fb;
--card: #ffffff;
--line: #d9dde5;
--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: 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>
<div class=\"wrap\">
<div class=\"head\">
<div>
<h2 style=\"margin:0;\">DroneDetector Telemetry Monitor</h2>
<div class=\"meta\">Green: dBFS current, Red: channel threshold, Red dots: alarm points</div>
</div>
<div class=\"meta\" id=\"status\">connecting...</div>
</div>
<div class=\"grid\" id=\"plots\"></div>
</div>
<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-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) {
const arr = state[freq] || [];
const cutoff = Date.now() / 1000 - windowSec;
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 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 (${labelSuffix})`,
line: {color: '#12b76a', width: 2},
},
{
x,
y: thr,
mode: 'lines',
name: `Threshold (${labelSuffix})`,
line: {color: '#ef4444', width: 2, dash: 'dash'},
},
{
x: alarmX,
y: alarmY,
mode: 'markers',
name: 'Alarm',
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',
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() {
const freqs = Object.keys(state).sort(numericSortFreq);
freqs.forEach(render);
}
async function loadInitial() {
const res = await fetch(`/telemetry/history?seconds=${windowSec}`);
const payload = await res.json();
const series = payload.series || {};
for (const [freq, points] of Object.entries(series)) {
state[freq] = points;
}
renderAll();
}
function connectWs() {
const proto = location.protocol === 'https:' ? 'wss' : 'ws';
const ws = new WebSocket(`${proto}://${location.host}/telemetry/ws`);
ws.onopen = () => {
document.getElementById('status').textContent = 'ws connected';
setInterval(() => {
if (ws.readyState === 1) ws.send('ping');
}, 20000);
};
ws.onmessage = (event) => {
const msg = JSON.parse(event.data);
if (msg.type === 'snapshot' && msg.data) {
for (const [freq, points] of Object.entries(msg.data)) {
state[freq] = points;
}
renderAll();
return;
}
if (msg.type !== 'point') return;
const p = msg.data;
const freq = String(p.freq);
if (!state[freq]) state[freq] = [];
state[freq].push(p);
render(freq);
};
ws.onclose = () => {
document.getElementById('status').textContent = 'ws disconnected, retrying...';
setTimeout(connectWs, 1500);
};
ws.onerror = () => {
document.getElementById('status').textContent = 'ws error';
};
}
setInterval(() => {
renderAll();
}, 1000);
loadInitial().then(connectWs).catch((e) => {
document.getElementById('status').textContent = `init error: ${e}`;
connectWs();
});
</script>
</body>
</html>
"""
@app.get('/', response_class=HTMLResponse)
@app.get('/monitor', response_class=HTMLResponse)
async def monitor_page():
return HTMLResponse(content=MONITOR_HTML)
if __name__ == '__main__':
import uvicorn
uvicorn.run(app, host=TELEMETRY_BIND_HOST, port=TELEMETRY_BIND_PORT)
Loading…
Cancel
Save