Sergey Revyakin 2 weeks ago
parent b8ab9b366d
commit ce43f5f8ff

@ -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,5 +1,4 @@
import os import os
import math
import statistics import statistics
# Более лучшая версия кода есть в FRScanner # Более лучшая версия кода есть в FRScanner
@ -12,28 +11,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: Процентный показатель превышения сигналом уровня шума. multiply_factor: Процентный показатель превышения сигналом уровня шума (legacy).
num_for_alarm: Количество раз, превышающих шум, при которых триггеримся. num_for_alarm: Количество раз, превышающих шум, при которых триггеримся.
is_init: Флаг инициализации буфера. = True, если инициализирован. is_init: Флаг инициализации буфера.
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
@ -43,29 +34,44 @@ 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_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.prev_values = [None] * self.line_size self.prev_values = [None] * self.line_size
self.trend_streak = [0] * self.line_size self.trend_streak = [0] * self.line_size
# Рост в 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_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.dbfs_min_trend_steps = int(os.getenv('dbfs_min_trend_steps', max(1, self.num_for_alarm)))
self.freq_tag = '' if freq_tag is None else str(freq_tag)
suffix = f'_{self.freq_tag}' if self.freq_tag else ''
# Параметры MAD-порогов (per-frequency с fallback на общие).
self.mad_k_on = float(os.getenv('mad_k_on' + suffix, os.getenv('mad_k_on', 5.0)))
self.mad_k_off = float(os.getenv('mad_k_off' + suffix, os.getenv('mad_k_off', 2.5)))
self.mad_eps = float(os.getenv('mad_eps' + suffix, os.getenv('mad_eps', 0.05)))
def get_buffer(self): def get_buffer(self):
return self.buffer return self.buffer
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
@ -75,23 +81,46 @@ 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)
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]))
self.buffer_medians[i] = med
self.buffer_mads[i] = float(self._calc_mad(self.buffer[i], med))
def get_threshold(self, channel_idx, k=None):
"""
Получить динамический порог в dB для канала:
threshold = median + k * MAD.
До завершения инициализации возвращает None.
"""
if not self.check_init():
return None
coef = self.mad_k_on if k is None else float(k)
baseline = float(self.buffer_medians[channel_idx])
mad = max(float(self.buffer_mads[channel_idx]), self.mad_eps)
return baseline + coef * mad
def get_thresholds(self, k=None):
if not self.check_init():
return [None] * self.line_size
return [self.get_threshold(i, k) for i in range(self.line_size)]
def alarms_fill_zeros(self): def alarms_fill_zeros(self):
self.buffer_alarms = [0] * self.line_size self.buffer_alarms = [0] * self.line_size
self.trend_streak = [0] * self.line_size self.trend_streak = [0] * self.line_size
self.prev_values = [None] * 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):
""" """
@ -128,15 +157,15 @@ class DataBuffer:
def check_alarm(self, data): def check_alarm(self, data):
""" """
Проверка триггера системы по dBFS во времени. Проверка триггера системы по dBFS во времени.
Триггер: устойчивый рост относительно фоновой медианы не меньше dbfs_delta_percent, Триггер: превышение динамического MAD-порога
подтвержденный несколькими последовательными чтениями. с подтверждением тренда и несколькими последовательными чтениями.
""" """
if self.check_init(): if self.check_init():
self.last_alarm_channels = []
for i in range(len(data)): for i in range(len(data)):
baseline = self.buffer_medians[i]
current = data[i] current = data[i]
threshold_on = self.get_threshold(i, self.mad_k_on)
growth_ratio = self._dbfs_growth_ratio(current, baseline) threshold_off = self.get_threshold(i, self.mad_k_off)
prev = self.prev_values[i] prev = self.prev_values[i]
delta_db = 0.0 if prev is None else current - prev delta_db = 0.0 if prev is None else current - prev
@ -147,8 +176,10 @@ class DataBuffer:
else: else:
self.trend_streak[i] = 0 self.trend_streak[i] = 0
# Hysteresis: после начала серии используем более мягкий порог отпускания.
active_threshold = threshold_off if self.buffer_alarms[i] > 0 else threshold_on
exceeding = ( exceeding = (
growth_ratio >= self.dbfs_delta_ratio current >= active_threshold
and self.trend_streak[i] >= self.dbfs_min_trend_steps and self.trend_streak[i] >= self.dbfs_min_trend_steps
) )
@ -160,6 +191,7 @@ class DataBuffer:
self.prev_values[i] = current self.prev_values[i] = current
if self.buffer_alarms[i] >= self.num_for_alarm: if self.buffer_alarms[i] >= self.num_for_alarm:
self.last_alarm_channels = [i]
self.buffer_alarms = [0] * self.line_size self.buffer_alarms = [0] * self.line_size
self.trend_streak = [0] * self.line_size self.trend_streak = [0] * self.line_size
return True return True
@ -168,15 +200,13 @@ class DataBuffer:
def check_single_alarm(self, median, cur_channel): def check_single_alarm(self, median, cur_channel):
""" """
Проверка, является ли текущая метрика по каналу превышающей порог роста. Проверка, является ли текущая метрика по каналу превышающей MAD-порог.
:param median: текущая метрика в dBFS. :param median: текущая метрика в dBFS.
:param cur_channel: индекс канала внутри частоты. :param cur_channel: индекс канала внутри частоты.
:return: Да/нет. :return: Да/нет.
""" """
if self.check_init(): if self.check_init():
baseline = self.buffer_medians[cur_channel] threshold_on = self.get_threshold(cur_channel, self.mad_k_on)
exceeding = self._dbfs_growth_ratio(median, baseline) >= self.dbfs_delta_ratio return median >= threshold_on
if exceeding:
return True return False
else:
return False

@ -1,168 +1,176 @@
import os import os
from core.data_buffer import DataBuffer from core.data_buffer import DataBuffer
def get_centre_freq(freq): def get_centre_freq(freq):
""" """
Получить название частоты по ее диапазону. Получить название частоты по ее диапазону.
:param freq: Частота, которую обрабатываем. :param freq: Частота, которую обрабатываем.
:return: Название частоты. :return: Название частоты.
""" """
c_freq = 0 c_freq = 0
if 5.46e9 <= freq <= 6.0e9: if 5.46e9 <= freq <= 6.0e9:
c_freq = 5800 c_freq = 5800
if 5.0e9 <= freq <= 5.4e9: if 5.0e9 <= freq <= 5.4e9:
c_freq = 5200 c_freq = 5200
if 4.5e9 <= freq <= 4.7e9: if 4.5e9 <= freq <= 4.7e9:
c_freq = 4500 c_freq = 4500
if 3.3e9 <= freq <= 3.5e9: if 3.3e9 <= freq <= 3.5e9:
c_freq = 3300 c_freq = 3300
if 2.4e9 <= freq <= 2.5e9: if 2.4e9 <= freq <= 2.5e9:
c_freq = 2400 c_freq = 2400
if 1e9 <= freq <= 1.36e9: if 1e9 <= freq <= 1.36e9:
c_freq = 1200 c_freq = 1200
if 0.9e9 <= freq <= 0.960e9: if 0.9e9 <= freq <= 0.960e9:
c_freq = 915 c_freq = 915
if 0.830e9 <= freq <= 0.890e9: if 0.830e9 <= freq <= 0.890e9:
c_freq = 868 c_freq = 868
if 0.700e9 <= freq <= 0.780e9: if 0.700e9 <= freq <= 0.780e9:
c_freq = 750 c_freq = 750
if 0.380e9 <= freq <= 0.500e9: if 0.380e9 <= freq <= 0.500e9:
c_freq = 433 c_freq = 433
return str(c_freq) return str(c_freq)
class MultiChannel: class MultiChannel:
""" """
Класс с реализацией переключателя каналов. Присутствует поддержка нескольких частот, а поэтому Класс с реализацией переключателя каналов. Присутствует поддержка нескольких частот, а поэтому
Атрибуты: Атрибуты:
steps: Массив шагов для разных частот. Ex. steps = [-20e6, -5e6, -3e6], i-ый элемент соответствует i-ой steps: Массив шагов для разных частот. Ex. steps = [-20e6, -5e6, -3e6], i-ый элемент соответствует i-ой
частоте для обработке, типа 1.2, 915 и 868. частоте для обработке, типа 1.2, 915 и 868.
bases: Массив верхних границ диапазонов рассматриваемых частот. Ex bases = [1.36e9, 0.93e9, 0.87e9] для bases: Массив верхних границ диапазонов рассматриваемых частот. Ex bases = [1.36e9, 0.93e9, 0.87e9] для
1.2, 915 и 868. 1.2, 915 и 868.
roofs: То же самое, только нижних границ. Ex roofs = [1e9, 0.9e9, 0.85e9] roofs: То же самое, только нижних границ. Ex roofs = [1e9, 0.9e9, 0.85e9]
cur_channel: Указатель на текущий канал, который обрабатываем. cur_channel: Указатель на текущий канал, который обрабатываем.
cur_roof: Указатель на нижнюю границу текущей обрабатываемой частоты. cur_roof: Указатель на нижнюю границу текущей обрабатываемой частоты.
cur_step: Указатель на шаг текущей обрабатываемой частоты. cur_step: Указатель на шаг текущей обрабатываемой частоты.
num_chs: Массив из каналов по обрабатываемым частотам. Вычисляется автоматически исходя из границ и шага. num_chs: Массив из каналов по обрабатываемым частотам. Вычисляется автоматически исходя из границ и шага.
init_freq: Чекер на инициализацию частоты перед началом работы скрипта. Нужен из-за особенности init_freq: Чекер на инициализацию частоты перед началом работы скрипта. Нужен из-за особенности
работы графов GNURadio и функции work в embedded Python блоке. работы графов GNURadio и функции work в embedded Python блоке.
DB: Список из циклических буферов для соответствующих чатсот. DB: Список из циклических буферов для соответствующих чатсот.
""" """
def __init__(self, steps, bases, roofs): def __init__(self, steps, bases, roofs):
""" """
Инициализация класса. Инициализация класса.
:param steps: Список с шагами для соответствующих частот. :param steps: Список с шагами для соответствующих частот.
:param bases: Список верхних границ диапазонов частот, с которыми работаем. :param bases: Список верхних границ диапазонов частот, с которыми работаем.
:param roofs: Список нижних границ --//--. :param roofs: Список нижних границ --//--.
""" """
self.steps = steps self.steps = steps
self.bases = bases self.bases = bases
self.roofs = roofs self.roofs = roofs
self.cur_channel = self.bases[0] self.cur_channel = self.bases[0]
self.cur_roof = self.roofs[0] self.cur_roof = self.roofs[0]
self.cur_step = self.steps[0] self.cur_step = self.steps[0]
self.num_chs = [] self.num_chs = []
self.init_freq = False self.init_freq = False
self.DB = [] self.DB = []
def init_f(self): def init_f(self):
""" """
Инициализация начальной частоты, с которой начинаем обработку. Инициализация начальной частоты, с которой начинаем обработку.
:return: Верхняя граница первой частоты из набора частот. :return: Верхняя граница первой частоты из набора частот.
""" """
self.init_freq = True self.init_freq = True
return self.bases[0] return self.bases[0]
def get_cur_channel(self): def get_cur_channel(self):
""" """
Получить текущий обрабатываемый канал. Получить текущий обрабатываемый канал.
:return: Канал обработки. :return: Канал обработки.
""" """
return self.cur_channel return self.cur_channel
def change_channel(self): def change_channel(self):
""" """
Функция смены канала. Идет от верхней границы диапазона частоты к нижней с шагом step. Если дошли до нижней Функция смены канала. Идет от верхней границы диапазона частоты к нижней с шагом step. Если дошли до нижней
границы, то переключаемся на следующую частоту посредством переноса курсора текущего канала на верхнюю границу границы, то переключаемся на следующую частоту посредством переноса курсора текущего канала на верхнюю границу
новой частоты и указатель нижней границы также двигаем на следующую позицию. Если частота для обработки одна, то новой частоты и указатель нижней границы также двигаем на следующую позицию. Если частота для обработки одна, то
указатель текущего канала возвращается в начало - верхней границы этой же частоты. Указатель нижней границы не указатель текущего канала возвращается в начало - верхней границы этой же частоты. Указатель нижней границы не
изменяется. изменяется.
:return: Канал после смены. :return: Канал после смены.
""" """
if not self.init_freq: if not self.init_freq:
return self.init_f() return self.init_f()
if self.cur_channel <= self.cur_roof: if self.cur_channel <= self.cur_roof:
if self.cur_roof == self.roofs[-1]: if self.cur_roof == self.roofs[-1]:
self.cur_channel = self.bases[0] self.cur_channel = self.bases[0]
self.cur_roof = self.roofs[0] self.cur_roof = self.roofs[0]
self.cur_step = self.steps[0] self.cur_step = self.steps[0]
else: else:
next_roofs = self.roofs.index(self.cur_roof) + 1 next_roofs = self.roofs.index(self.cur_roof) + 1
self.cur_channel = self.bases[next_roofs] self.cur_channel = self.bases[next_roofs]
self.cur_roof = self.roofs[next_roofs] self.cur_roof = self.roofs[next_roofs]
self.cur_step = self.steps[next_roofs] self.cur_step = self.steps[next_roofs]
else: else:
self.cur_channel += self.cur_step self.cur_channel += self.cur_step
# print('Канал частоты изменен на ', self.cur_channel / 1000000) # print('Канал частоты изменен на ', self.cur_channel / 1000000)
return self.get_cur_channel() return self.get_cur_channel()
def get_num_chs(self, idx_freq): def get_num_chs(self, idx_freq):
""" """
Вычисляет количество каналов на частоте исходя из верхнего, нижнего диапазонов и шага. Вычисляет количество каналов на частоте исходя из верхнего, нижнего диапазонов и шага.
:param idx_freq: id частоты внутри класса. Т.е. в данный момент обрабатывается несколько частот, то id = :param idx_freq: id частоты внутри класса. Т.е. в данный момент обрабатывается несколько частот, то id =
индексу верхней границы в bases для данной частоты, или нижней границы в roofs или шагу в steps. индексу верхней границы в bases для данной частоты, или нижней границы в roofs или шагу в steps.
В примерах из описания атрибутов индекс частоты 915 будет равен единице (т.к. идет вторым элементом в списках). В примерах из описания атрибутов индекс частоты 915 будет равен единице (т.к. идет вторым элементом в списках).
:return: Количество каналов. :return: Количество каналов.
""" """
if (idx_freq + 1) > len(self.num_chs): if (idx_freq + 1) > len(self.num_chs):
tmp = self.bases[idx_freq] tmp = self.bases[idx_freq]
counter = 0 counter = 0
while tmp >= self.roofs[idx_freq]: while tmp >= self.roofs[idx_freq]:
counter += 1 counter += 1
tmp += self.steps[idx_freq] tmp += self.steps[idx_freq]
self.num_chs.append(counter) self.num_chs.append(counter)
return counter return counter
else: else:
return self.num_chs[idx_freq] return self.num_chs[idx_freq]
def check_f(self, freq): def check_f(self, freq):
""" """
Проверить наличие частоты в классе. Если да, то вернуть количество каналов и циклический буфер этой частоты. Проверить наличие частоты в классе. Если да, то вернуть количество каналов и циклический буфер этой частоты.
:param freq: Частота. :param freq: Частота.
:return: Количество каналов, циклический буфер выбранной частоты ИЛИ none. :return: Количество каналов, циклический буфер выбранной частоты ИЛИ none.
""" """
for i in range(len(self.bases)): for i in range(len(self.bases)):
if self.roofs[i] <= freq <= self.bases[i]: if self.roofs[i] <= freq <= self.bases[i]:
return self.get_num_chs(i), self.DB[i] return self.get_num_chs(i), self.DB[i]
else: else:
return None, None return None, None
def fill_DB(self): def fill_DB(self):
""" """
Инициализировать циклические буферы для всех частот в отдельный список. Инициализировать циклические буферы для всех частот в отдельный список.
:return: N0nE. :return: N0nE.
""" """
for i in range(len(self.bases)): for i in range(len(self.bases)):
freq = get_centre_freq(self.bases[i]) freq = get_centre_freq(self.bases[i])
buffer_columns_size = int(os.getenv('buffer_columns_size_' + str(freq))) buffer_columns_size = int(os.getenv('buffer_columns_size_' + str(freq)))
num_of_thinning_iter = int(os.getenv('num_of_thinning_iter_' + str(freq))) num_of_thinning_iter = int(os.getenv('num_of_thinning_iter_' + str(freq)))
multiply_factor = float(os.getenv('multiply_factor_' + str(freq))) multiply_factor = float(os.getenv('multiply_factor_' + str(freq)))
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,
def db_alarms_zeros(self, circle_buffer): num_of_thinning_iter,
""" num_chs,
При отработке системы зануляет алармы во всех буферах, кроме текущего, т.к. в текущем уже занулилось. multiply_factor,
:param circle_buffer: Циклический буфер текущей обрабатываемой частоты. num_for_alarm,
:return: None. freq_tag=str(freq),
""" )
for i in range(len(self.DB)): )
if self.DB[i] != circle_buffer:
self.DB[i].alarms_fill_zeros() def db_alarms_zeros(self, circle_buffer):
"""
При отработке системы зануляет алармы во всех буферах, кроме текущего, т.к. в текущем уже занулилось.
:param circle_buffer: Циклический буфер текущей обрабатываемой частоты.
:return: None.
"""
for i in range(len(self.DB)):
if self.DB[i] != circle_buffer:
self.DB[i].alarms_fill_zeros()

@ -1,6 +1,5 @@
import os import os
import datetime import datetime
import math
import time 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
@ -53,7 +52,6 @@ telemetry_host = os.getenv('telemetry_host', '127.0.0.1')
telemetry_port = os.getenv('telemetry_port', '5020') telemetry_port = os.getenv('telemetry_port', '5020')
telemetry_endpoint = os.getenv('telemetry_endpoint', 'telemetry') telemetry_endpoint = os.getenv('telemetry_endpoint', 'telemetry')
telemetry_timeout_sec = float(os.getenv('telemetry_timeout_sec', '0.30')) telemetry_timeout_sec = float(os.getenv('telemetry_timeout_sec', '0.30'))
telemetry_delta_percent = float(os.getenv('dbfs_delta_percent', '15'))
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(',')
@ -100,13 +98,9 @@ def work(lvl):
max_idx = max(range(len(sigs_array)), key=lambda idx: sigs_array[idx]) max_idx = max(range(len(sigs_array)), key=lambda idx: sigs_array[idx])
dbfs_current = float(sigs_array[max_idx]) dbfs_current = float(sigs_array[max_idx])
dbfs_threshold = None dbfs_threshold = circle_buffer.get_threshold(max_idx)
if circle_buffer.check_init(): channel_thresholds = circle_buffer.get_thresholds()
medians = circle_buffer.get_medians() alarm_channels = circle_buffer.get_last_alarm_channels() if alarm else []
baseline = float(medians[max_idx])
dbfs_threshold = baseline + 10.0 * math.log10(
1.0 + telemetry_delta_percent / 100.0
)
send_telemetry( send_telemetry(
data={ data={
@ -117,6 +111,9 @@ def work(lvl):
"alarm": bool(alarm), "alarm": bool(alarm),
"channel_idx": int(max_idx), "channel_idx": int(max_idx),
"channels_total": int(len(sigs_array)), "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, host=telemetry_host,
port=telemetry_port, port=telemetry_port,

@ -1,15 +1,11 @@
import os import os
import datetime import datetime
import math
import time 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, send_telemetry, save_data, remote_save_data from utils.datas_processing import pack_elems, agregator, send_data, send_telemetry, save_data, remote_save_data
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
import logging
logging.basicConfig(level=logging.INFO)
load_root_env(__file__) load_root_env(__file__)
freq_suffix = os.path.splitext(os.path.basename(__file__))[0].split("_")[-1] freq_suffix = os.path.splitext(os.path.basename(__file__))[0].split("_")[-1]
@ -56,7 +52,6 @@ telemetry_host = os.getenv('telemetry_host', '127.0.0.1')
telemetry_port = os.getenv('telemetry_port', '5020') telemetry_port = os.getenv('telemetry_port', '5020')
telemetry_endpoint = os.getenv('telemetry_endpoint', 'telemetry') telemetry_endpoint = os.getenv('telemetry_endpoint', 'telemetry')
telemetry_timeout_sec = float(os.getenv('telemetry_timeout_sec', '0.30')) telemetry_timeout_sec = float(os.getenv('telemetry_timeout_sec', '0.30'))
telemetry_delta_percent = float(os.getenv('dbfs_delta_percent', '15'))
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(',')
@ -103,15 +98,12 @@ def work(lvl):
max_idx = max(range(len(sigs_array)), key=lambda idx: sigs_array[idx]) max_idx = max(range(len(sigs_array)), key=lambda idx: sigs_array[idx])
dbfs_current = float(sigs_array[max_idx]) dbfs_current = float(sigs_array[max_idx])
dbfs_threshold = None dbfs_threshold = circle_buffer.get_threshold(max_idx)
if circle_buffer.check_init(): channel_thresholds = circle_buffer.get_thresholds()
medians = circle_buffer.get_medians() alarm_channels = circle_buffer.get_last_alarm_channels() if alarm else []
baseline = float(medians[max_idx])
dbfs_threshold = baseline + 10.0 * math.log10(
1.0 + telemetry_delta_percent / 100.0
)
data={ send_telemetry(
data={
"freq": str(freq), "freq": str(freq),
"ts": time.time(), "ts": time.time(),
"dbfs_current": dbfs_current, "dbfs_current": dbfs_current,
@ -119,17 +111,15 @@ def work(lvl):
"alarm": bool(alarm), "alarm": bool(alarm),
"channel_idx": int(max_idx), "channel_idx": int(max_idx),
"channels_total": int(len(sigs_array)), "channels_total": int(len(sigs_array)),
} "channel_values": [float(v) for v in sigs_array],
"channel_thresholds": channel_thresholds,
send_telemetry( "alarm_channels": alarm_channels,
data=data },
host=telemetry_host, host=telemetry_host,
port=telemetry_port, port=telemetry_port,
endpoint=telemetry_endpoint, endpoint=telemetry_endpoint,
timeout_sec=telemetry_timeout_sec, timeout_sec=telemetry_timeout_sec,
) )
logging.info(data)
except Exception as exc: except Exception as exc:
if debug_flag: if debug_flag:
print(f"telemetry send failed: {exc}") print(f"telemetry send failed: {exc}")

@ -1,6 +1,5 @@
import os import os
import datetime import datetime
import math
import time 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
@ -53,7 +52,6 @@ telemetry_host = os.getenv('telemetry_host', '127.0.0.1')
telemetry_port = os.getenv('telemetry_port', '5020') telemetry_port = os.getenv('telemetry_port', '5020')
telemetry_endpoint = os.getenv('telemetry_endpoint', 'telemetry') telemetry_endpoint = os.getenv('telemetry_endpoint', 'telemetry')
telemetry_timeout_sec = float(os.getenv('telemetry_timeout_sec', '0.30')) telemetry_timeout_sec = float(os.getenv('telemetry_timeout_sec', '0.30'))
telemetry_delta_percent = float(os.getenv('dbfs_delta_percent', '15'))
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(',')
@ -100,13 +98,9 @@ def work(lvl):
max_idx = max(range(len(sigs_array)), key=lambda idx: sigs_array[idx]) max_idx = max(range(len(sigs_array)), key=lambda idx: sigs_array[idx])
dbfs_current = float(sigs_array[max_idx]) dbfs_current = float(sigs_array[max_idx])
dbfs_threshold = None dbfs_threshold = circle_buffer.get_threshold(max_idx)
if circle_buffer.check_init(): channel_thresholds = circle_buffer.get_thresholds()
medians = circle_buffer.get_medians() alarm_channels = circle_buffer.get_last_alarm_channels() if alarm else []
baseline = float(medians[max_idx])
dbfs_threshold = baseline + 10.0 * math.log10(
1.0 + telemetry_delta_percent / 100.0
)
send_telemetry( send_telemetry(
data={ data={
@ -117,6 +111,9 @@ def work(lvl):
"alarm": bool(alarm), "alarm": bool(alarm),
"channel_idx": int(max_idx), "channel_idx": int(max_idx),
"channels_total": int(len(sigs_array)), "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, host=telemetry_host,
port=telemetry_port, port=telemetry_port,

@ -1,6 +1,5 @@
import os import os
import datetime import datetime
import math
import time 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
@ -53,7 +52,6 @@ telemetry_host = os.getenv('telemetry_host', '127.0.0.1')
telemetry_port = os.getenv('telemetry_port', '5020') telemetry_port = os.getenv('telemetry_port', '5020')
telemetry_endpoint = os.getenv('telemetry_endpoint', 'telemetry') telemetry_endpoint = os.getenv('telemetry_endpoint', 'telemetry')
telemetry_timeout_sec = float(os.getenv('telemetry_timeout_sec', '0.30')) telemetry_timeout_sec = float(os.getenv('telemetry_timeout_sec', '0.30'))
telemetry_delta_percent = float(os.getenv('dbfs_delta_percent', '15'))
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(',')
@ -100,13 +98,9 @@ def work(lvl):
max_idx = max(range(len(sigs_array)), key=lambda idx: sigs_array[idx]) max_idx = max(range(len(sigs_array)), key=lambda idx: sigs_array[idx])
dbfs_current = float(sigs_array[max_idx]) dbfs_current = float(sigs_array[max_idx])
dbfs_threshold = None dbfs_threshold = circle_buffer.get_threshold(max_idx)
if circle_buffer.check_init(): channel_thresholds = circle_buffer.get_thresholds()
medians = circle_buffer.get_medians() alarm_channels = circle_buffer.get_last_alarm_channels() if alarm else []
baseline = float(medians[max_idx])
dbfs_threshold = baseline + 10.0 * math.log10(
1.0 + telemetry_delta_percent / 100.0
)
send_telemetry( send_telemetry(
data={ data={
@ -117,6 +111,9 @@ def work(lvl):
"alarm": bool(alarm), "alarm": bool(alarm),
"channel_idx": int(max_idx), "channel_idx": int(max_idx),
"channels_total": int(len(sigs_array)), "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, host=telemetry_host,
port=telemetry_port, port=telemetry_port,

@ -1,6 +1,5 @@
import os import os
import datetime import datetime
import math
import time 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
@ -53,7 +52,6 @@ telemetry_host = os.getenv('telemetry_host', '127.0.0.1')
telemetry_port = os.getenv('telemetry_port', '5020') telemetry_port = os.getenv('telemetry_port', '5020')
telemetry_endpoint = os.getenv('telemetry_endpoint', 'telemetry') telemetry_endpoint = os.getenv('telemetry_endpoint', 'telemetry')
telemetry_timeout_sec = float(os.getenv('telemetry_timeout_sec', '0.30')) telemetry_timeout_sec = float(os.getenv('telemetry_timeout_sec', '0.30'))
telemetry_delta_percent = float(os.getenv('dbfs_delta_percent', '15'))
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(',')
@ -100,13 +98,9 @@ def work(lvl):
max_idx = max(range(len(sigs_array)), key=lambda idx: sigs_array[idx]) max_idx = max(range(len(sigs_array)), key=lambda idx: sigs_array[idx])
dbfs_current = float(sigs_array[max_idx]) dbfs_current = float(sigs_array[max_idx])
dbfs_threshold = None dbfs_threshold = circle_buffer.get_threshold(max_idx)
if circle_buffer.check_init(): channel_thresholds = circle_buffer.get_thresholds()
medians = circle_buffer.get_medians() alarm_channels = circle_buffer.get_last_alarm_channels() if alarm else []
baseline = float(medians[max_idx])
dbfs_threshold = baseline + 10.0 * math.log10(
1.0 + telemetry_delta_percent / 100.0
)
send_telemetry( send_telemetry(
data={ data={
@ -117,6 +111,9 @@ def work(lvl):
"alarm": bool(alarm), "alarm": bool(alarm),
"channel_idx": int(max_idx), "channel_idx": int(max_idx),
"channels_total": int(len(sigs_array)), "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, host=telemetry_host,
port=telemetry_port, port=telemetry_port,

@ -1,6 +1,5 @@
import os import os
import datetime import datetime
import math
import time 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
@ -53,7 +52,6 @@ telemetry_host = os.getenv('telemetry_host', '127.0.0.1')
telemetry_port = os.getenv('telemetry_port', '5020') telemetry_port = os.getenv('telemetry_port', '5020')
telemetry_endpoint = os.getenv('telemetry_endpoint', 'telemetry') telemetry_endpoint = os.getenv('telemetry_endpoint', 'telemetry')
telemetry_timeout_sec = float(os.getenv('telemetry_timeout_sec', '0.30')) telemetry_timeout_sec = float(os.getenv('telemetry_timeout_sec', '0.30'))
telemetry_delta_percent = float(os.getenv('dbfs_delta_percent', '15'))
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(',')
@ -103,13 +101,9 @@ def work(lvl):
max_idx = max(range(len(sigs_array)), key=lambda idx: sigs_array[idx]) max_idx = max(range(len(sigs_array)), key=lambda idx: sigs_array[idx])
dbfs_current = float(sigs_array[max_idx]) dbfs_current = float(sigs_array[max_idx])
dbfs_threshold = None dbfs_threshold = circle_buffer.get_threshold(max_idx)
if circle_buffer.check_init(): channel_thresholds = circle_buffer.get_thresholds()
medians = circle_buffer.get_medians() alarm_channels = circle_buffer.get_last_alarm_channels() if alarm else []
baseline = float(medians[max_idx])
dbfs_threshold = baseline + 10.0 * math.log10(
1.0 + telemetry_delta_percent / 100.0
)
send_telemetry( send_telemetry(
data={ data={
@ -120,6 +114,9 @@ def work(lvl):
"alarm": bool(alarm), "alarm": bool(alarm),
"channel_idx": int(max_idx), "channel_idx": int(max_idx),
"channels_total": int(len(sigs_array)), "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, host=telemetry_host,
port=telemetry_port, port=telemetry_port,

@ -1,6 +1,5 @@
import os import os
import datetime import datetime
import math
import time 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
@ -53,7 +52,6 @@ telemetry_host = os.getenv('telemetry_host', '127.0.0.1')
telemetry_port = os.getenv('telemetry_port', '5020') telemetry_port = os.getenv('telemetry_port', '5020')
telemetry_endpoint = os.getenv('telemetry_endpoint', 'telemetry') telemetry_endpoint = os.getenv('telemetry_endpoint', 'telemetry')
telemetry_timeout_sec = float(os.getenv('telemetry_timeout_sec', '0.30')) telemetry_timeout_sec = float(os.getenv('telemetry_timeout_sec', '0.30'))
telemetry_delta_percent = float(os.getenv('dbfs_delta_percent', '15'))
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(',')
@ -100,13 +98,9 @@ def work(lvl):
max_idx = max(range(len(sigs_array)), key=lambda idx: sigs_array[idx]) max_idx = max(range(len(sigs_array)), key=lambda idx: sigs_array[idx])
dbfs_current = float(sigs_array[max_idx]) dbfs_current = float(sigs_array[max_idx])
dbfs_threshold = None dbfs_threshold = circle_buffer.get_threshold(max_idx)
if circle_buffer.check_init(): channel_thresholds = circle_buffer.get_thresholds()
medians = circle_buffer.get_medians() alarm_channels = circle_buffer.get_last_alarm_channels() if alarm else []
baseline = float(medians[max_idx])
dbfs_threshold = baseline + 10.0 * math.log10(
1.0 + telemetry_delta_percent / 100.0
)
send_telemetry( send_telemetry(
data={ data={
@ -117,6 +111,9 @@ def work(lvl):
"alarm": bool(alarm), "alarm": bool(alarm),
"channel_idx": int(max_idx), "channel_idx": int(max_idx),
"channels_total": int(len(sigs_array)), "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, host=telemetry_host,
port=telemetry_port, port=telemetry_port,

@ -36,6 +36,9 @@ class TelemetryPoint(BaseModel):
alarm: bool = False alarm: bool = False
channel_idx: int = 0 channel_idx: int = 0
channels_total: int = 1 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: def _prune_freq_locked(freq: str, now_ts: float) -> None:
@ -136,15 +139,25 @@ MONITOR_HTML = """
--text: #1c232e; --text: #1c232e;
--green: #12b76a; --green: #12b76a;
--red: #ef4444; --red: #ef4444;
--muted: #5b6574;
} }
body { margin: 0; background: var(--bg); color: var(--text); font-family: system-ui, -apple-system, Segoe UI, sans-serif; } 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; } .wrap { max-width: 1800px; margin: 0 auto; padding: 14px; }
.head { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; } .head { display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; }
.meta { font-size: 13px; color: #5b6574; } .meta { font-size: 13px; color: var(--muted); }
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(420px, 1fr)); gap: 12px; } .grid { display: flex; flex-direction: column; gap: 10px; }
.card { background: var(--card); border: 1px solid var(--line); border-radius: 10px; padding: 8px 8px 2px; } .card { width: 100%; background: var(--card); border: 1px solid var(--line); border-radius: 10px; padding: 8px 8px 8px; }
.title { font-size: 14px; font-weight: 600; margin: 6px 8px; } .title-row { display: flex; justify-content: space-between; align-items: center; margin: 4px 8px; }
.plot { height: 280px; } .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> </style>
</head> </head>
<body> <body>
@ -152,7 +165,7 @@ MONITOR_HTML = """
<div class=\"head\"> <div class=\"head\">
<div> <div>
<h2 style=\"margin:0;\">DroneDetector Telemetry Monitor</h2> <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>
<div class=\"meta\" id=\"status\">connecting...</div> <div class=\"meta\" id=\"status\">connecting...</div>
</div> </div>
@ -162,17 +175,77 @@ MONITOR_HTML = """
<script> <script>
const windowSec = 300; const windowSec = 300;
const state = {}; // freq -> points[] const state = {}; // freq -> points[]
const selectedChannel = {}; // freq -> 'max' | channel index as string
function numericSortFreq(a, b) { function numericSortFreq(a, b) {
return Number(a) - Number(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) { function ensurePlot(freq) {
if (document.getElementById(`plot-${freq}`)) return; if (document.getElementById(`plot-${freq}`)) return;
const card = document.createElement('div'); const card = document.createElement('div');
card.className = 'card'; 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); 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) { function trimPoints(freq) {
@ -181,47 +254,134 @@ function trimPoints(freq) {
state[freq] = arr.filter(p => Number(p.ts) >= cutoff); 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) { function render(freq) {
ensurePlot(freq); ensurePlot(freq);
trimPoints(freq); trimPoints(freq);
updateChannelSelector(freq);
const pts = state[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 ts = new Date(Number(p.ts) * 1000);
const y = pts.map(p => p.dbfs_current); x.push(ts);
const thr = pts.map(p => p.dbfs_threshold); y.push(metric.y);
const alarmPts = pts.filter(p => p.alarm === true); thr.push(metric.threshold);
if (isAlarmForSelection(p, sel)) {
alarmX.push(ts);
alarmY.push(metric.y);
}
}
const labelSuffix = sel === 'max' ? 'max' : `ch ${sel}`;
const traces = [ const traces = [
{ {
x, x,
y, y,
mode: 'lines', mode: 'lines',
name: 'dBFS', name: `dBFS (${labelSuffix})`,
line: {color: '#12b76a', width: 2} line: {color: '#12b76a', width: 2},
}, },
{ {
x, x,
y: thr, y: thr,
mode: 'lines', mode: 'lines',
name: 'Threshold', name: `Threshold (${labelSuffix})`,
line: {color: '#ef4444', width: 2, dash: 'dash'} line: {color: '#ef4444', width: 2, dash: 'dash'},
}, },
{ {
x: alarmPts.map(p => new Date(Number(p.ts) * 1000)), x: alarmX,
y: alarmPts.map(p => p.dbfs_current), y: alarmY,
mode: 'markers', mode: 'markers',
name: 'Alarm', name: 'Alarm',
marker: {color: '#ef4444', size: 6, symbol: 'circle'} marker: {color: '#ef4444', size: 6, symbol: 'circle'},
} },
]; ];
Plotly.react(`plot-${freq}`, traces, { Plotly.react(`plot-${freq}`, traces, {
margin: {l: 40, r: 12, t: 12, b: 32}, margin: {l: 40, r: 12, t: 12, b: 32},
showlegend: true, showlegend: true,
legend: {orientation: 'h', y: 1.16}, legend: {orientation: 'h', y: 1.16},
xaxis: {title: 'time'}, xaxis: {
yaxis: {title: 'dBFS'} 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}); }, {displayModeBar: false, responsive: true});
renderAlarmEvents(freq, pts);
} }
function renderAll() { function renderAll() {
@ -259,7 +419,9 @@ function connectWs() {
renderAll(); renderAll();
return; return;
} }
if (msg.type !== 'point') return; if (msg.type !== 'point') return;
const p = msg.data; const p = msg.data;
const freq = String(p.freq); const freq = String(p.freq);
if (!state[freq]) state[freq] = []; if (!state[freq]) state[freq] = [];
@ -277,6 +439,10 @@ function connectWs() {
}; };
} }
setInterval(() => {
renderAll();
}, 1000);
loadInitial().then(connectWs).catch((e) => { loadInitial().then(connectWs).catch((e) => {
document.getElementById('status').textContent = `init error: ${e}`; document.getElementById('status').textContent = `init error: ${e}`;
connectWs(); connectWs();

Loading…
Cancel
Save