diff --git a/restart_all.sh b/restart_all.sh index 0955a95..2098b09 100755 --- a/restart_all.sh +++ b/restart_all.sh @@ -7,14 +7,14 @@ COMPOSE_FILE="${PROJECT_ROOT}/deploy/docker/docker-compose.yml" SDR_UNITS=( dronedetector-sdr-433.service dronedetector-sdr-750.service - dronedetector-sdr-868.service + #dronedetector-sdr-868.service dronedetector-sdr-3300.service - dronedetector-sdr-4500.service + #dronedetector-sdr-4500.service dronedetector-sdr-5200.service dronedetector-sdr-5800.service - dronedetector-sdr-915.service - dronedetector-sdr-1200.service - dronedetector-sdr-2400.service + #dronedetector-sdr-915.service + #dronedetector-sdr-1200.service + #dronedetector-sdr-2400.service ) log() { diff --git a/src/core/data_buffer.py b/src/core/data_buffer.py index 8980709..388b0b5 100644 --- a/src/core/data_buffer.py +++ b/src/core/data_buffer.py @@ -1,5 +1,4 @@ import os -import math import statistics # Более лучшая версия кода есть в FRScanner @@ -12,28 +11,20 @@ class DataBuffer: Атрибуты: current_column: Указатель на текущий столбец буфера, который обновляем. thinning_counter: Прореживающий множитель на текующей итерации. - current_counter: Указатель на количество чтений между последним обновлением столбца и предыдущим атрибутом. - num_of_thinning_iter: Прореживающий множитель. Раз в это количечество раз будет обнволяться столбец буфера. + current_counter: Указатель на количество чтений между последним обновлением столбца и предыдущим атрибутом. + num_of_thinning_iter: Прореживающий множитель. Раз в это количество раз будет обновляться столбец буфера. line_size: Количество строк буфера = количеству каналов. columns_size: Количество столбцов = фиксированное число. - multiply_factor: Процентный показатель превышения сигналом уровня шума. + multiply_factor: Процентный показатель превышения сигналом уровня шума (legacy). num_for_alarm: Количество раз, превышающих шум, при которых триггеримся. - is_init: Флаг инициализации буфера. = True, если инициализирован. + is_init: Флаг инициализации буфера. buffer: Массив для буфера. - buffer_medians: Массив для медиан столбцов букера. - buffer_alarms: Массив для количества тревог по столбца буфера. + buffer_medians: Массив медиан по каналам. + buffer_mads: Массив MAD по каналам. + buffer_alarms: Массив для количества тревог по каналам. """ - def __init__(self, columns_size, num_of_thinning_iter, num_of_channels, multiply_factor, num_for_alarm): - """ - Инициализируем класс. - - :param columns_size: - :param num_of_thinning_iter: - :param num_of_channels: - :param multiply_factor: - :param num_for_alarm: - """ + def __init__(self, columns_size, num_of_thinning_iter, num_of_channels, multiply_factor, num_for_alarm, freq_tag=None): self.current_column = 0 self.thinning_counter = 1 self.current_counter = 1 @@ -43,29 +34,44 @@ class DataBuffer: self.multiply_factor = multiply_factor self.num_for_alarm = num_for_alarm self.is_init = False + self.buffer = [[0 for _ in range(self.columns_size)] for _ in range(self.line_size)] - self.buffer_medians = [0] * self.line_size + self.buffer_medians = [0.0] * self.line_size + self.buffer_mads = [0.0] * self.line_size self.buffer_alarms = [0] * self.line_size + self.last_alarm_channels = [] self.prev_values = [None] * 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_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): return self.buffer def get_medians(self): return self.buffer_medians + def get_mads(self): + return self.buffer_mads + def get_alarms(self): return self.buffer_alarms + def get_last_alarm_channels(self): + return list(self.last_alarm_channels) + def check_init(self): return self.is_init @@ -75,23 +81,46 @@ class DataBuffer: print(self.buffer[i], end=' ') print() + @staticmethod + def _calc_mad(values, median): + deviations = [abs(v - median) for v in values] + return statistics.median(deviations) + def medians(self): """ - Вычислить медиану по строке буфера. + Вычислить медиану и MAD по строкам буфера. :return: None """ if self.check_init(): for i in range(self.line_size): - self.buffer_medians[i] = statistics.median(self.buffer[i]) + med = float(statistics.median(self.buffer[i])) + self.buffer_medians[i] = med + self.buffer_mads[i] = float(self._calc_mad(self.buffer[i], med)) + + def get_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): self.buffer_alarms = [0] * self.line_size self.trend_streak = [0] * self.line_size self.prev_values = [None] * self.line_size - - @staticmethod - def _dbfs_growth_ratio(current_db, baseline_db): - return math.pow(10.0, (current_db - baseline_db) / 10.0) - 1.0 + self.last_alarm_channels = [] def update(self, data): """ @@ -128,15 +157,15 @@ class DataBuffer: def check_alarm(self, data): """ Проверка триггера системы по dBFS во времени. - Триггер: устойчивый рост относительно фоновой медианы не меньше dbfs_delta_percent, - подтвержденный несколькими последовательными чтениями. + Триггер: превышение динамического MAD-порога + с подтверждением тренда и несколькими последовательными чтениями. """ if self.check_init(): + self.last_alarm_channels = [] for i in range(len(data)): - baseline = self.buffer_medians[i] current = data[i] - - growth_ratio = self._dbfs_growth_ratio(current, baseline) + threshold_on = self.get_threshold(i, self.mad_k_on) + threshold_off = self.get_threshold(i, self.mad_k_off) prev = self.prev_values[i] delta_db = 0.0 if prev is None else current - prev @@ -147,8 +176,10 @@ class DataBuffer: else: self.trend_streak[i] = 0 + # Hysteresis: после начала серии используем более мягкий порог отпускания. + active_threshold = threshold_off if self.buffer_alarms[i] > 0 else threshold_on exceeding = ( - growth_ratio >= self.dbfs_delta_ratio + current >= active_threshold and self.trend_streak[i] >= self.dbfs_min_trend_steps ) @@ -160,6 +191,7 @@ class DataBuffer: self.prev_values[i] = current if self.buffer_alarms[i] >= self.num_for_alarm: + self.last_alarm_channels = [i] self.buffer_alarms = [0] * self.line_size self.trend_streak = [0] * self.line_size return True @@ -168,15 +200,13 @@ class DataBuffer: def check_single_alarm(self, median, cur_channel): """ - Проверка, является ли текущая метрика по каналу превышающей порог роста. + Проверка, является ли текущая метрика по каналу превышающей MAD-порог. :param median: текущая метрика в dBFS. :param cur_channel: индекс канала внутри частоты. :return: Да/нет. """ if self.check_init(): - baseline = self.buffer_medians[cur_channel] - exceeding = self._dbfs_growth_ratio(median, baseline) >= self.dbfs_delta_ratio - if exceeding: - return True - else: - return False + threshold_on = self.get_threshold(cur_channel, self.mad_k_on) + return median >= threshold_on + + return False diff --git a/src/core/multichannelswitcher.py b/src/core/multichannelswitcher.py index ed6107c..bb936f0 100644 --- a/src/core/multichannelswitcher.py +++ b/src/core/multichannelswitcher.py @@ -1,168 +1,176 @@ -import os -from core.data_buffer import DataBuffer - - -def get_centre_freq(freq): - """ - Получить название частоты по ее диапазону. - :param freq: Частота, которую обрабатываем. - :return: Название частоты. - """ - c_freq = 0 - if 5.46e9 <= freq <= 6.0e9: - c_freq = 5800 - if 5.0e9 <= freq <= 5.4e9: - c_freq = 5200 - if 4.5e9 <= freq <= 4.7e9: - c_freq = 4500 - if 3.3e9 <= freq <= 3.5e9: - c_freq = 3300 - if 2.4e9 <= freq <= 2.5e9: - c_freq = 2400 - if 1e9 <= freq <= 1.36e9: - c_freq = 1200 - if 0.9e9 <= freq <= 0.960e9: - c_freq = 915 - if 0.830e9 <= freq <= 0.890e9: - c_freq = 868 - if 0.700e9 <= freq <= 0.780e9: - c_freq = 750 - if 0.380e9 <= freq <= 0.500e9: - c_freq = 433 - return str(c_freq) - - -class MultiChannel: - """ - Класс с реализацией переключателя каналов. Присутствует поддержка нескольких частот, а поэтому - Атрибуты: - steps: Массив шагов для разных частот. Ex. steps = [-20e6, -5e6, -3e6], i-ый элемент соответствует i-ой - частоте для обработке, типа 1.2, 915 и 868. - bases: Массив верхних границ диапазонов рассматриваемых частот. Ex bases = [1.36e9, 0.93e9, 0.87e9] для - 1.2, 915 и 868. - roofs: То же самое, только нижних границ. Ex roofs = [1e9, 0.9e9, 0.85e9] - cur_channel: Указатель на текущий канал, который обрабатываем. - cur_roof: Указатель на нижнюю границу текущей обрабатываемой частоты. - cur_step: Указатель на шаг текущей обрабатываемой частоты. - num_chs: Массив из каналов по обрабатываемым частотам. Вычисляется автоматически исходя из границ и шага. - init_freq: Чекер на инициализацию частоты перед началом работы скрипта. Нужен из-за особенности - работы графов GNURadio и функции work в embedded Python блоке. - DB: Список из циклических буферов для соответствующих чатсот. - """ - - def __init__(self, steps, bases, roofs): - """ - Инициализация класса. - :param steps: Список с шагами для соответствующих частот. - :param bases: Список верхних границ диапазонов частот, с которыми работаем. - :param roofs: Список нижних границ --//--. - """ - - self.steps = steps - self.bases = bases - self.roofs = roofs - self.cur_channel = self.bases[0] - self.cur_roof = self.roofs[0] - self.cur_step = self.steps[0] - self.num_chs = [] - self.init_freq = False - self.DB = [] - - def init_f(self): - """ - Инициализация начальной частоты, с которой начинаем обработку. - :return: Верхняя граница первой частоты из набора частот. - """ - self.init_freq = True - return self.bases[0] - - def get_cur_channel(self): - """ - Получить текущий обрабатываемый канал. - :return: Канал обработки. - """ - return self.cur_channel - - def change_channel(self): - """ - Функция смены канала. Идет от верхней границы диапазона частоты к нижней с шагом step. Если дошли до нижней - границы, то переключаемся на следующую частоту посредством переноса курсора текущего канала на верхнюю границу - новой частоты и указатель нижней границы также двигаем на следующую позицию. Если частота для обработки одна, то - указатель текущего канала возвращается в начало - верхней границы этой же частоты. Указатель нижней границы не - изменяется. - - :return: Канал после смены. - """ - if not self.init_freq: - return self.init_f() - - if self.cur_channel <= self.cur_roof: - if self.cur_roof == self.roofs[-1]: - self.cur_channel = self.bases[0] - self.cur_roof = self.roofs[0] - self.cur_step = self.steps[0] - else: - next_roofs = self.roofs.index(self.cur_roof) + 1 - self.cur_channel = self.bases[next_roofs] - self.cur_roof = self.roofs[next_roofs] - self.cur_step = self.steps[next_roofs] - else: - self.cur_channel += self.cur_step - # print('Канал частоты изменен на ', self.cur_channel / 1000000) - return self.get_cur_channel() - - def get_num_chs(self, idx_freq): - """ - Вычисляет количество каналов на частоте исходя из верхнего, нижнего диапазонов и шага. - :param idx_freq: id частоты внутри класса. Т.е. в данный момент обрабатывается несколько частот, то id = - индексу верхней границы в bases для данной частоты, или нижней границы в roofs или шагу в steps. - В примерах из описания атрибутов индекс частоты 915 будет равен единице (т.к. идет вторым элементом в списках). - :return: Количество каналов. - """ - if (idx_freq + 1) > len(self.num_chs): - tmp = self.bases[idx_freq] - counter = 0 - while tmp >= self.roofs[idx_freq]: - counter += 1 - tmp += self.steps[idx_freq] - self.num_chs.append(counter) - return counter - else: - return self.num_chs[idx_freq] - - def check_f(self, freq): - """ - Проверить наличие частоты в классе. Если да, то вернуть количество каналов и циклический буфер этой частоты. - :param freq: Частота. - :return: Количество каналов, циклический буфер выбранной частоты ИЛИ none. - """ - for i in range(len(self.bases)): - if self.roofs[i] <= freq <= self.bases[i]: - return self.get_num_chs(i), self.DB[i] - else: - return None, None - - def fill_DB(self): - """ - Инициализировать циклические буферы для всех частот в отдельный список. - :return: N0nE. - """ - for i in range(len(self.bases)): - freq = get_centre_freq(self.bases[i]) - buffer_columns_size = int(os.getenv('buffer_columns_size_' + str(freq))) - num_of_thinning_iter = int(os.getenv('num_of_thinning_iter_' + str(freq))) - multiply_factor = float(os.getenv('multiply_factor_' + str(freq))) - num_for_alarm = int(os.getenv('num_for_alarm_' + str(freq))) - num_chs = self.get_num_chs(i) - self.DB.append( - DataBuffer(buffer_columns_size, num_of_thinning_iter, num_chs, multiply_factor, num_for_alarm)) - - 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() +import os +from core.data_buffer import DataBuffer + + +def get_centre_freq(freq): + """ + Получить название частоты по ее диапазону. + :param freq: Частота, которую обрабатываем. + :return: Название частоты. + """ + c_freq = 0 + if 5.46e9 <= freq <= 6.0e9: + c_freq = 5800 + if 5.0e9 <= freq <= 5.4e9: + c_freq = 5200 + if 4.5e9 <= freq <= 4.7e9: + c_freq = 4500 + if 3.3e9 <= freq <= 3.5e9: + c_freq = 3300 + if 2.4e9 <= freq <= 2.5e9: + c_freq = 2400 + if 1e9 <= freq <= 1.36e9: + c_freq = 1200 + if 0.9e9 <= freq <= 0.960e9: + c_freq = 915 + if 0.830e9 <= freq <= 0.890e9: + c_freq = 868 + if 0.700e9 <= freq <= 0.780e9: + c_freq = 750 + if 0.380e9 <= freq <= 0.500e9: + c_freq = 433 + return str(c_freq) + + +class MultiChannel: + """ + Класс с реализацией переключателя каналов. Присутствует поддержка нескольких частот, а поэтому + Атрибуты: + steps: Массив шагов для разных частот. Ex. steps = [-20e6, -5e6, -3e6], i-ый элемент соответствует i-ой + частоте для обработке, типа 1.2, 915 и 868. + bases: Массив верхних границ диапазонов рассматриваемых частот. Ex bases = [1.36e9, 0.93e9, 0.87e9] для + 1.2, 915 и 868. + roofs: То же самое, только нижних границ. Ex roofs = [1e9, 0.9e9, 0.85e9] + cur_channel: Указатель на текущий канал, который обрабатываем. + cur_roof: Указатель на нижнюю границу текущей обрабатываемой частоты. + cur_step: Указатель на шаг текущей обрабатываемой частоты. + num_chs: Массив из каналов по обрабатываемым частотам. Вычисляется автоматически исходя из границ и шага. + init_freq: Чекер на инициализацию частоты перед началом работы скрипта. Нужен из-за особенности + работы графов GNURadio и функции work в embedded Python блоке. + DB: Список из циклических буферов для соответствующих чатсот. + """ + + def __init__(self, steps, bases, roofs): + """ + Инициализация класса. + :param steps: Список с шагами для соответствующих частот. + :param bases: Список верхних границ диапазонов частот, с которыми работаем. + :param roofs: Список нижних границ --//--. + """ + + self.steps = steps + self.bases = bases + self.roofs = roofs + self.cur_channel = self.bases[0] + self.cur_roof = self.roofs[0] + self.cur_step = self.steps[0] + self.num_chs = [] + self.init_freq = False + self.DB = [] + + def init_f(self): + """ + Инициализация начальной частоты, с которой начинаем обработку. + :return: Верхняя граница первой частоты из набора частот. + """ + self.init_freq = True + return self.bases[0] + + def get_cur_channel(self): + """ + Получить текущий обрабатываемый канал. + :return: Канал обработки. + """ + return self.cur_channel + + def change_channel(self): + """ + Функция смены канала. Идет от верхней границы диапазона частоты к нижней с шагом step. Если дошли до нижней + границы, то переключаемся на следующую частоту посредством переноса курсора текущего канала на верхнюю границу + новой частоты и указатель нижней границы также двигаем на следующую позицию. Если частота для обработки одна, то + указатель текущего канала возвращается в начало - верхней границы этой же частоты. Указатель нижней границы не + изменяется. + + :return: Канал после смены. + """ + if not self.init_freq: + return self.init_f() + + if self.cur_channel <= self.cur_roof: + if self.cur_roof == self.roofs[-1]: + self.cur_channel = self.bases[0] + self.cur_roof = self.roofs[0] + self.cur_step = self.steps[0] + else: + next_roofs = self.roofs.index(self.cur_roof) + 1 + self.cur_channel = self.bases[next_roofs] + self.cur_roof = self.roofs[next_roofs] + self.cur_step = self.steps[next_roofs] + else: + self.cur_channel += self.cur_step + # print('Канал частоты изменен на ', self.cur_channel / 1000000) + return self.get_cur_channel() + + def get_num_chs(self, idx_freq): + """ + Вычисляет количество каналов на частоте исходя из верхнего, нижнего диапазонов и шага. + :param idx_freq: id частоты внутри класса. Т.е. в данный момент обрабатывается несколько частот, то id = + индексу верхней границы в bases для данной частоты, или нижней границы в roofs или шагу в steps. + В примерах из описания атрибутов индекс частоты 915 будет равен единице (т.к. идет вторым элементом в списках). + :return: Количество каналов. + """ + if (idx_freq + 1) > len(self.num_chs): + tmp = self.bases[idx_freq] + counter = 0 + while tmp >= self.roofs[idx_freq]: + counter += 1 + tmp += self.steps[idx_freq] + self.num_chs.append(counter) + return counter + else: + return self.num_chs[idx_freq] + + def check_f(self, freq): + """ + Проверить наличие частоты в классе. Если да, то вернуть количество каналов и циклический буфер этой частоты. + :param freq: Частота. + :return: Количество каналов, циклический буфер выбранной частоты ИЛИ none. + """ + for i in range(len(self.bases)): + if self.roofs[i] <= freq <= self.bases[i]: + return self.get_num_chs(i), self.DB[i] + else: + return None, None + + def fill_DB(self): + """ + Инициализировать циклические буферы для всех частот в отдельный список. + :return: N0nE. + """ + for i in range(len(self.bases)): + freq = get_centre_freq(self.bases[i]) + buffer_columns_size = int(os.getenv('buffer_columns_size_' + str(freq))) + num_of_thinning_iter = int(os.getenv('num_of_thinning_iter_' + str(freq))) + multiply_factor = float(os.getenv('multiply_factor_' + str(freq))) + num_for_alarm = int(os.getenv('num_for_alarm_' + str(freq))) + num_chs = self.get_num_chs(i) + self.DB.append( + DataBuffer( + buffer_columns_size, + num_of_thinning_iter, + num_chs, + multiply_factor, + num_for_alarm, + freq_tag=str(freq), + ) + ) + + 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() diff --git a/src/embedded_3300.py b/src/embedded_3300.py index c85ac91..4165edf 100644 --- a/src/embedded_3300.py +++ b/src/embedded_3300.py @@ -1,6 +1,5 @@ import os import datetime -import math import time from common.runtime import load_root_env, validate_env, as_bool, as_str 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_endpoint = os.getenv('telemetry_endpoint', 'telemetry') 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(',') 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]) dbfs_current = float(sigs_array[max_idx]) - dbfs_threshold = None - if circle_buffer.check_init(): - medians = circle_buffer.get_medians() - baseline = float(medians[max_idx]) - dbfs_threshold = baseline + 10.0 * math.log10( - 1.0 + telemetry_delta_percent / 100.0 - ) + 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={ @@ -117,6 +111,9 @@ def work(lvl): "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, diff --git a/src/embedded_433.py b/src/embedded_433.py index 09105e3..0471e22 100644 --- a/src/embedded_433.py +++ b/src/embedded_433.py @@ -1,15 +1,11 @@ import os import datetime -import math import time from common.runtime import load_root_env, validate_env, as_bool, as_str from smb.SMBConnection import SMBConnection from utils.datas_processing import pack_elems, agregator, send_data, send_telemetry, save_data, remote_save_data from core.sig_n_medi_collect import Signal, SignalsArray, get_signal_length from core.multichannelswitcher import MultiChannel, get_centre_freq -import logging - -logging.basicConfig(level=logging.INFO) load_root_env(__file__) 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_endpoint = os.getenv('telemetry_endpoint', 'telemetry') 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(',') 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]) dbfs_current = float(sigs_array[max_idx]) - dbfs_threshold = None - if circle_buffer.check_init(): - medians = circle_buffer.get_medians() - baseline = float(medians[max_idx]) - dbfs_threshold = baseline + 10.0 * math.log10( - 1.0 + telemetry_delta_percent / 100.0 - ) + 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 [] - data={ + send_telemetry( + data={ "freq": str(freq), "ts": time.time(), "dbfs_current": dbfs_current, @@ -119,17 +111,15 @@ def work(lvl): "alarm": bool(alarm), "channel_idx": int(max_idx), "channels_total": int(len(sigs_array)), - } - - send_telemetry( - data=data + "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, ) - logging.info(data) - except Exception as exc: if debug_flag: print(f"telemetry send failed: {exc}") diff --git a/src/embedded_4500.py b/src/embedded_4500.py index 9d94432..298dbcd 100644 --- a/src/embedded_4500.py +++ b/src/embedded_4500.py @@ -1,6 +1,5 @@ import os import datetime -import math import time from common.runtime import load_root_env, validate_env, as_bool, as_str 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_endpoint = os.getenv('telemetry_endpoint', 'telemetry') 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(',') 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]) dbfs_current = float(sigs_array[max_idx]) - dbfs_threshold = None - if circle_buffer.check_init(): - medians = circle_buffer.get_medians() - baseline = float(medians[max_idx]) - dbfs_threshold = baseline + 10.0 * math.log10( - 1.0 + telemetry_delta_percent / 100.0 - ) + 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={ @@ -117,6 +111,9 @@ def work(lvl): "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, diff --git a/src/embedded_5200.py b/src/embedded_5200.py index 97fb708..576a7fb 100644 --- a/src/embedded_5200.py +++ b/src/embedded_5200.py @@ -1,6 +1,5 @@ import os import datetime -import math import time from common.runtime import load_root_env, validate_env, as_bool, as_str 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_endpoint = os.getenv('telemetry_endpoint', 'telemetry') 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(',') 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]) dbfs_current = float(sigs_array[max_idx]) - dbfs_threshold = None - if circle_buffer.check_init(): - medians = circle_buffer.get_medians() - baseline = float(medians[max_idx]) - dbfs_threshold = baseline + 10.0 * math.log10( - 1.0 + telemetry_delta_percent / 100.0 - ) + 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={ @@ -117,6 +111,9 @@ def work(lvl): "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, diff --git a/src/embedded_5800.py b/src/embedded_5800.py index 1ce92c1..a87b1cd 100644 --- a/src/embedded_5800.py +++ b/src/embedded_5800.py @@ -1,6 +1,5 @@ import os import datetime -import math import time from common.runtime import load_root_env, validate_env, as_bool, as_str 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_endpoint = os.getenv('telemetry_endpoint', 'telemetry') 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(',') 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]) dbfs_current = float(sigs_array[max_idx]) - dbfs_threshold = None - if circle_buffer.check_init(): - medians = circle_buffer.get_medians() - baseline = float(medians[max_idx]) - dbfs_threshold = baseline + 10.0 * math.log10( - 1.0 + telemetry_delta_percent / 100.0 - ) + 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={ @@ -117,6 +111,9 @@ def work(lvl): "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, diff --git a/src/embedded_750.py b/src/embedded_750.py index 5aa6bc9..8ead9b9 100644 --- a/src/embedded_750.py +++ b/src/embedded_750.py @@ -1,6 +1,5 @@ import os import datetime -import math import time from common.runtime import load_root_env, validate_env, as_bool, as_str 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_endpoint = os.getenv('telemetry_endpoint', 'telemetry') 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(',') 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]) dbfs_current = float(sigs_array[max_idx]) - dbfs_threshold = None - if circle_buffer.check_init(): - medians = circle_buffer.get_medians() - baseline = float(medians[max_idx]) - dbfs_threshold = baseline + 10.0 * math.log10( - 1.0 + telemetry_delta_percent / 100.0 - ) + 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={ @@ -120,6 +114,9 @@ def work(lvl): "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, diff --git a/src/embedded_868.py b/src/embedded_868.py index 7942576..fea476a 100644 --- a/src/embedded_868.py +++ b/src/embedded_868.py @@ -1,6 +1,5 @@ import os import datetime -import math import time from common.runtime import load_root_env, validate_env, as_bool, as_str 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_endpoint = os.getenv('telemetry_endpoint', 'telemetry') 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(',') 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]) dbfs_current = float(sigs_array[max_idx]) - dbfs_threshold = None - if circle_buffer.check_init(): - medians = circle_buffer.get_medians() - baseline = float(medians[max_idx]) - dbfs_threshold = baseline + 10.0 * math.log10( - 1.0 + telemetry_delta_percent / 100.0 - ) + 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={ @@ -117,6 +111,9 @@ def work(lvl): "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, diff --git a/telemetry/telemetry_server.py b/telemetry/telemetry_server.py index 1cacf14..d507fb4 100644 --- a/telemetry/telemetry_server.py +++ b/telemetry/telemetry_server.py @@ -36,6 +36,9 @@ class TelemetryPoint(BaseModel): alarm: bool = False channel_idx: int = 0 channels_total: int = 1 + channel_values: Optional[List[float]] = None + channel_thresholds: Optional[List[Optional[float]]] = None + alarm_channels: Optional[List[int]] = None def _prune_freq_locked(freq: str, now_ts: float) -> None: @@ -136,15 +139,25 @@ MONITOR_HTML = """ --text: #1c232e; --green: #12b76a; --red: #ef4444; + --muted: #5b6574; } body { margin: 0; background: var(--bg); color: var(--text); font-family: system-ui, -apple-system, Segoe UI, sans-serif; } - .wrap { max-width: 1400px; margin: 0 auto; padding: 16px; } - .head { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; } - .meta { font-size: 13px; color: #5b6574; } - .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(420px, 1fr)); gap: 12px; } - .card { background: var(--card); border: 1px solid var(--line); border-radius: 10px; padding: 8px 8px 2px; } - .title { font-size: 14px; font-weight: 600; margin: 6px 8px; } - .plot { height: 280px; } + .wrap { max-width: 1800px; margin: 0 auto; padding: 14px; } + .head { display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; } + .meta { font-size: 13px; color: var(--muted); } + .grid { display: flex; flex-direction: column; gap: 10px; } + .card { width: 100%; background: var(--card); border: 1px solid var(--line); border-radius: 10px; padding: 8px 8px 8px; } + .title-row { display: flex; justify-content: space-between; align-items: center; margin: 4px 8px; } + .title { font-size: 20px; font-weight: 700; } + .ctrl { display: flex; align-items: center; gap: 6px; } + .ctrl label { font-size: 12px; color: var(--muted); } + .ctrl select { border: 1px solid var(--line); border-radius: 6px; padding: 2px 6px; } + .plot { height: 260px; width: 100%; } + .events-title { font-size: 12px; color: var(--muted); margin: 2px 8px 4px; } + .events { max-height: 110px; overflow-y: auto; border-top: 1px dashed var(--line); margin: 0 8px; padding-top: 4px; } + .ev { display: flex; justify-content: space-between; font-size: 12px; line-height: 1.4; color: var(--text); } + .ev-t { color: var(--muted); } + .ev-empty { color: var(--muted); font-size: 12px; }
@@ -152,7 +165,7 @@ MONITOR_HTML = """