Compare commits

..

10 Commits
main ... Ufa

@ -253,15 +253,29 @@ SERVER_PORT_2=8080
################# #################
# NN_SERVER # NN_SERVER
NN_MODEL_2400=ensemble_2_pic
NN_WEIGHTS_2400=${PATH_TO_NN}ensemble2400_2pic.pth
NN_CLASSES_2400=drone,noise
NN_MODEL_1200=ensemble_2_pic
NN_WEIGHTS_1200=${PATH_TO_NN}ensemble1200_2pic.pth
NN_CLASSES_1200=drone,noise
NN_MODEL_915=ensemble_2_pic
NN_WEIGHTS_915=${PATH_TO_NN}ensemble915_2pic.pth
NN_CLASSES_915=drone,noise
NN_BUILD_FUNC=build_func_ensemble
NN_PRE_FUNC=pre_func_ensemble
NN_INFERENCE_FUNC=inference_func_ensemble
NN_POST_FUNC=post_func_ensemble
NN_SYNTHETIC_EXAMPLES=10
NN_SYNTHETIC_MIX_COUNT=1
NN_SRC_DATASET=/app/NN_server/datasets/full_dataset/
################# #################
FREQS=915,1200,2400 FREQS=915,1200,2400
PATH_TO_NN=/app/NN_server/NN/ PATH_TO_NN=/app/NN_server/NN/
SRC_RESULT=/app/NN_server/result/ SRC_RESULT=/app/NN_server/result/
SRC_EXAMPLE=${PATH_TO_NN}example/ SRC_EXAMPLE=${PATH_TO_NN}example/
NN_1='${PATH_TO_NN}resnet18_1.pth && ${PATH_TO_NN}config_resnet18.yaml && ${SRC_EXAMPLE} && ${SRC_RESULT} && Resnet18_1_2400 && build_func_resnet18 && pre_func_resnet18 && inference_func_resnet18 && post_func_resnet18 && [drone,noise,wifi] && 10 && 1 && /app/NN_server/datasets/full_dataset_pic/'
NN_21='${PATH_TO_NN}ensemble_1.2.pth && ${PATH_TO_NN}config_ensemble.yaml && ${SRC_EXAMPLE} && ${SRC_RESULT} && ensemble_1200 && build_func_ensemble && pre_func_ensemble && inference_func_ensemble && post_func_ensemble && [drone,noise] && 10 && 1 && /app/NN_server/datasets/full_dataset/'
NN_22='${PATH_TO_NN}ensemble_915.pth && ${PATH_TO_NN}config_ensemble.yaml && ${SRC_EXAMPLE} && ${SRC_RESULT} && ensemble_915 && build_func_ensemble && pre_func_ensemble && inference_func_ensemble && post_func_ensemble && [drone,noise] && 10 && 1 && /app/NN_server/datasets/full_dataset/'
GENERAL_SERVER_IP=dronedetector-server-to-master GENERAL_SERVER_IP=dronedetector-server-to-master
GENERAL_SERVER_PORT=5010 GENERAL_SERVER_PORT=5010

@ -99,10 +99,11 @@ class Model(object):
except Exception as exc: except Exception as exc:
print(str(exc)) print(str(exc))
def __init__(self, file_model='', file_config='', src_example='', src_result='', type_model='', def __init__(self, freq=0, file_model='', file_config='', src_example='', src_result='', type_model='',
build_model_func=None, pre_func=None, inference_func=None, post_func=None, classes=None, build_model_func=None, pre_func=None, inference_func=None, post_func=None, classes=None,
number_synthetic_examples=0, number_src_data_for_one_synthetic_example=0, path_to_src_dataset=''): number_synthetic_examples=0, number_src_data_for_one_synthetic_example=0, path_to_src_dataset=''):
try: try:
self._freq = int(freq)
self._file_model = file_model self._file_model = file_model
self._file_config = file_config self._file_config = file_config
self._src_example = src_example self._src_example = src_example
@ -137,6 +138,9 @@ class Model(object):
def get_mapping(self): def get_mapping(self):
return list(self._classes.values()) return list(self._classes.values())
def get_freq(self):
return self._freq
def get_model_name(self): def get_model_name(self):
return self._type_model return self._type_model

@ -0,0 +1 @@
from Models.ensemble_2_pic import *

@ -0,0 +1 @@
from Models.ensemble_2_pic import *

@ -1,197 +0,0 @@
from torchvision import models
import torch.nn as nn
import matplotlib
import numpy as np
import torch
import cv2
import gc
import io
def _render_plot(values, figsize=(16, 16), dpi=16):
import matplotlib.pyplot as plt
fig = plt.figure(figsize=figsize)
plt.axes(ylim=(-1, 1))
plt.plot(values, color="black")
plt.gca().set_axis_off()
plt.subplots_adjust(top=1, bottom=0, right=1, left=0, hspace=0, wspace=0)
plt.margins(0, 0)
buf = io.BytesIO()
fig.savefig(buf, format="png", dpi=dpi)
buf.seek(0)
img_arr = np.frombuffer(buf.getvalue(), dtype=np.uint8)
buf.close()
img = cv2.imdecode(img_arr, 1)
if img is None:
raise RuntimeError("failed to decode plot image")
plt.clf()
plt.cla()
plt.close()
plt.close(fig)
return np.asarray(cv2.split(img), dtype=np.float32)
def pre_func_ensemble(data=None, src="", ind_inference=0):
try:
import matplotlib.pyplot as plt
matplotlib.use("Agg")
plt.ioff()
real = np.asarray(data[0], dtype=np.float32)
imag = np.asarray(data[1], dtype=np.float32)
signal = real + 1j * imag
img_real = _render_plot(signal.real)
img_mag = _render_plot(np.abs(signal))
cv2.destroyAllWindows()
gc.collect()
print("Подготовка данных завершена")
print()
return [img_real, img_mag]
except Exception as exc:
print(str(exc))
return None
def build_func_ensemble(file_model="", file_config="", num_classes=None):
try:
import matplotlib.pyplot as plt
matplotlib.use("Agg")
plt.ioff()
torch.cuda.empty_cache()
num_classes = 2
model1 = models.resnet18(pretrained=False)
model2 = models.resnet50(pretrained=False)
model1.fc = nn.Linear(model1.fc.in_features, num_classes)
model2.fc = nn.Linear(model2.fc.in_features, num_classes)
class Ensemble(nn.Module):
def __init__(self, model1, model2):
super().__init__()
self.model1 = model1
self.model2 = model2
self.fc = nn.Linear(2 * num_classes, num_classes)
def forward(self, x):
if isinstance(x, (list, tuple)):
x1 = x[0]
x2 = x[1] if len(x) > 1 else x[0]
else:
x1 = x
x2 = x
y1 = self.model1(x1)
y2 = self.model2(x2)
y = torch.cat((y1, y2), dim=1)
return self.fc(y)
model = Ensemble(model1, model2)
device = "cuda" if torch.cuda.is_available() else "cpu"
if device != "cpu":
model = model.to(device)
model.load_state_dict(torch.load(file_model, map_location=device))
model.eval()
cv2.destroyAllWindows()
gc.collect()
print("Инициализация модели завершена")
print()
return model
except Exception as exc:
print(str(exc))
return None
def inference_func_ensemble(data=None, model=None, mapping=None, shablon=""):
try:
cv2.destroyAllWindows()
gc.collect()
torch.cuda.empty_cache()
device = "cuda" if torch.cuda.is_available() else "cpu"
if isinstance(data, (list, tuple)) and len(data) >= 2:
inputs = [
torch.unsqueeze(torch.tensor(data[0]).cpu(), 0).to(device).float(),
torch.unsqueeze(torch.tensor(data[1]).cpu(), 0).to(device).float(),
]
else:
tensor = torch.unsqueeze(torch.tensor(data).cpu(), 0).to(device).float()
inputs = [tensor, tensor]
with torch.no_grad():
output = model(inputs)
_, predict = torch.max(output.data, 1)
prediction = mapping[int(np.asarray(predict.cpu())[0])]
print("PREDICTION" + shablon + ": " + prediction)
output = output.cpu()
label = np.asarray(np.argmax(output, axis=1))[0]
output = np.asarray(torch.squeeze(output, 0))
expon = np.exp(output - np.max(output))
probability = round((expon / expon.sum())[label], 2)
cv2.destroyAllWindows()
gc.collect()
print("Уверенность" + shablon + " в предсказании: " + str(probability))
print("Инференс завершен")
print()
return [prediction, probability]
except Exception as exc:
print(str(exc))
return None
def post_func_ensemble(src="", model_type="", prediction="", model_id=0, ind_inference=0, data=None):
try:
import matplotlib.pyplot as plt
matplotlib.use("Agg")
plt.ioff()
if int(ind_inference) <= 100 and isinstance(data, (list, tuple)) and len(data) >= 2:
fig, ax = plt.subplots()
ax.imshow(np.moveaxis(data[0], 0, -1))
plt.savefig(src + "_inference_" + str(ind_inference) + "_" + prediction + "_real_" + str(model_id) + "_" + model_type + ".png")
plt.clf()
plt.cla()
plt.close(fig)
cv2.destroyAllWindows()
gc.collect()
fig, ax = plt.subplots()
ax.imshow(np.moveaxis(data[1], 0, -1))
plt.savefig(src + "_inference_" + str(ind_inference) + "_" + prediction + "_mod_" + str(model_id) + "_" + model_type + ".png")
plt.clf()
plt.cla()
plt.close(fig)
cv2.destroyAllWindows()
gc.collect()
plt.clf()
plt.cla()
plt.close()
cv2.destroyAllWindows()
gc.collect()
print("Постобработка завершена")
print()
except Exception as exc:
print(str(exc))
return None

@ -0,0 +1 @@
from Models.ensemble_2_pic import *

@ -1,197 +0,0 @@
from torchvision import models
import torch.nn as nn
import matplotlib
import numpy as np
import torch
import cv2
import gc
import io
def _render_plot(values, figsize=(16, 16), dpi=16):
import matplotlib.pyplot as plt
fig = plt.figure(figsize=figsize)
plt.axes(ylim=(-1, 1))
plt.plot(values, color="black")
plt.gca().set_axis_off()
plt.subplots_adjust(top=1, bottom=0, right=1, left=0, hspace=0, wspace=0)
plt.margins(0, 0)
buf = io.BytesIO()
fig.savefig(buf, format="png", dpi=dpi)
buf.seek(0)
img_arr = np.frombuffer(buf.getvalue(), dtype=np.uint8)
buf.close()
img = cv2.imdecode(img_arr, 1)
if img is None:
raise RuntimeError("failed to decode plot image")
plt.clf()
plt.cla()
plt.close()
plt.close(fig)
return np.asarray(cv2.split(img), dtype=np.float32)
def pre_func_ensemble(data=None, src="", ind_inference=0):
try:
import matplotlib.pyplot as plt
matplotlib.use("Agg")
plt.ioff()
real = np.asarray(data[0], dtype=np.float32)
imag = np.asarray(data[1], dtype=np.float32)
signal = real + 1j * imag
img_real = _render_plot(signal.real)
img_mag = _render_plot(np.abs(signal))
cv2.destroyAllWindows()
gc.collect()
print("Подготовка данных завершена")
print()
return [img_real, img_mag]
except Exception as exc:
print(str(exc))
return None
def build_func_ensemble(file_model="", file_config="", num_classes=None):
try:
import matplotlib.pyplot as plt
matplotlib.use("Agg")
plt.ioff()
torch.cuda.empty_cache()
num_classes = 2
model1 = models.resnet18(pretrained=False)
model2 = models.resnet50(pretrained=False)
model1.fc = nn.Linear(model1.fc.in_features, num_classes)
model2.fc = nn.Linear(model2.fc.in_features, num_classes)
class Ensemble(nn.Module):
def __init__(self, model1, model2):
super().__init__()
self.model1 = model1
self.model2 = model2
self.fc = nn.Linear(2 * num_classes, num_classes)
def forward(self, x):
if isinstance(x, (list, tuple)):
x1 = x[0]
x2 = x[1] if len(x) > 1 else x[0]
else:
x1 = x
x2 = x
y1 = self.model1(x1)
y2 = self.model2(x2)
y = torch.cat((y1, y2), dim=1)
return self.fc(y)
model = Ensemble(model1, model2)
device = "cuda" if torch.cuda.is_available() else "cpu"
if device != "cpu":
model = model.to(device)
model.load_state_dict(torch.load(file_model, map_location=device))
model.eval()
cv2.destroyAllWindows()
gc.collect()
print("Инициализация модели завершена")
print()
return model
except Exception as exc:
print(str(exc))
return None
def inference_func_ensemble(data=None, model=None, mapping=None, shablon=""):
try:
cv2.destroyAllWindows()
gc.collect()
torch.cuda.empty_cache()
device = "cuda" if torch.cuda.is_available() else "cpu"
if isinstance(data, (list, tuple)) and len(data) >= 2:
inputs = [
torch.unsqueeze(torch.tensor(data[0]).cpu(), 0).to(device).float(),
torch.unsqueeze(torch.tensor(data[1]).cpu(), 0).to(device).float(),
]
else:
tensor = torch.unsqueeze(torch.tensor(data).cpu(), 0).to(device).float()
inputs = [tensor, tensor]
with torch.no_grad():
output = model(inputs)
_, predict = torch.max(output.data, 1)
prediction = mapping[int(np.asarray(predict.cpu())[0])]
print("PREDICTION" + shablon + ": " + prediction)
output = output.cpu()
label = np.asarray(np.argmax(output, axis=1))[0]
output = np.asarray(torch.squeeze(output, 0))
expon = np.exp(output - np.max(output))
probability = round((expon / expon.sum())[label], 2)
cv2.destroyAllWindows()
gc.collect()
print("Уверенность" + shablon + " в предсказании: " + str(probability))
print("Инференс завершен")
print()
return [prediction, probability]
except Exception as exc:
print(str(exc))
return None
def post_func_ensemble(src="", model_type="", prediction="", model_id=0, ind_inference=0, data=None):
try:
import matplotlib.pyplot as plt
matplotlib.use("Agg")
plt.ioff()
if int(ind_inference) <= 100 and isinstance(data, (list, tuple)) and len(data) >= 2:
fig, ax = plt.subplots()
ax.imshow(np.moveaxis(data[0], 0, -1))
plt.savefig(src + "_inference_" + str(ind_inference) + "_" + prediction + "_real_" + str(model_id) + "_" + model_type + ".png")
plt.clf()
plt.cla()
plt.close(fig)
cv2.destroyAllWindows()
gc.collect()
fig, ax = plt.subplots()
ax.imshow(np.moveaxis(data[1], 0, -1))
plt.savefig(src + "_inference_" + str(ind_inference) + "_" + prediction + "_mod_" + str(model_id) + "_" + model_type + ".png")
plt.clf()
plt.cla()
plt.close(fig)
cv2.destroyAllWindows()
gc.collect()
plt.clf()
plt.cla()
plt.close()
cv2.destroyAllWindows()
gc.collect()
print("Постобработка завершена")
print()
except Exception as exc:
print(str(exc))
return None

@ -3,7 +3,6 @@ from dotenv import dotenv_values
from common.runtime import load_root_env, validate_env, as_int, as_str from common.runtime import load_root_env, validate_env, as_int, as_str
import os import os
import sys import sys
import re
import matplotlib.pyplot as plt import matplotlib.pyplot as plt
from Model import Model from Model import Model
import numpy as np import numpy as np
@ -11,25 +10,19 @@ import matplotlib
import importlib import importlib
import threading import threading
import requests import requests
import asyncio
import shutil import shutil
import json import json
import gc import gc
import logging import logging
import time
TORCHSIG_PATH = "/app/torchsig" TORCHSIG_PATH = "/app/torchsig"
if TORCHSIG_PATH not in sys.path: if TORCHSIG_PATH not in sys.path:
# Ensure import torchsig resolves to /app/torchsig/torchsig package.
sys.path.insert(0, TORCHSIG_PATH) sys.path.insert(0, TORCHSIG_PATH)
logging.basicConfig(level=logging.INFO) logging.basicConfig(level=logging.INFO)
app = Flask(__name__) app = Flask(__name__)
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
queue = asyncio.Queue()
semaphore = asyncio.Semaphore(3)
prediction_list = [] prediction_list = []
result_msg = {} result_msg = {}
results = [] results = []
@ -44,38 +37,39 @@ validate_env("NN_server/server.py", {
"GENERAL_SERVER_PORT": as_int, "GENERAL_SERVER_PORT": as_int,
"SERVER_IP": as_str, "SERVER_IP": as_str,
"SERVER_PORT": as_int, "SERVER_PORT": as_int,
"PATH_TO_NN": as_str,
"SRC_RESULT": as_str, "SRC_RESULT": as_str,
"SRC_EXAMPLE": as_str, "SRC_EXAMPLE": as_str,
"FREQS": as_str,
}) })
config = dict(dotenv_values(ROOT_ENV)) config = dict(dotenv_values(ROOT_ENV))
def is_model_config_key(key, value): def get_required_drone_streak(freq):
return bool(re.fullmatch(r"NN_\d+", key or "")) and isinstance(value, str) and " && " in value return config.get(f"DRONE_STREAK_{freq}", "1")
def get_required_drone_streak(freq): def get_required_drone_prob(freq):
raw_value = config.get(f"DRONE_STREAK_{freq}", "1") return config.get(f"DRONE_PROB_THRESHOLD_{freq}", config.get("DRONE_PROB_THRESHOLD_DEFAULT", "0"))
try:
return max(1, int(raw_value))
except (TypeError, ValueError):
logging.warning("Invalid DRONE_STREAK_%s=%r, falling back to 1", freq, raw_value)
return 1
def update_drone_streak(freq, prediction): def update_drone_streak(freq, prediction, drone_probability):
if prediction == "drone": required_prob = get_required_drone_prob(freq)
drone_probability = 0.0 if drone_probability is None else float(drone_probability)
passes_prob_gate = prediction == "drone" and drone_probability >= required_prob
if passes_prob_gate:
drone_streaks[freq] = drone_streaks.get(freq, 0) + 1 drone_streaks[freq] = drone_streaks.get(freq, 0) + 1
else: else:
drone_streaks[freq] = 0 drone_streaks[freq] = 0
required = get_required_drone_streak(freq) required = get_required_drone_streak(freq)
triggered = prediction == "drone" and drone_streaks[freq] >= required triggered = passes_prob_gate and drone_streaks[freq] >= required
logging.info( logging.info(
"NN alarm gate freq=%s prediction=%s streak=%s/%s triggered=%s", "NN alarm gate freq=%s prediction=%s drone_probability=%.3f threshold=%.3f streak=%s/%s triggered=%s",
freq, freq,
prediction, prediction,
drone_probability,
required_prob,
drone_streaks[freq], drone_streaks[freq],
required, required,
triggered, triggered,
@ -83,52 +77,171 @@ def update_drone_streak(freq, prediction):
return 8 if triggered else 0 return 8 if triggered else 0
def parse_freqs(raw_value):
freqs = []
for item in (raw_value or "").split(','):
item = item.strip()
if not item:
continue
freqs.append(int(item))
if not freqs:
raise RuntimeError("[NN_server/server.py] no NN frequencies configured in FREQS")
return freqs
def parse_classes(raw_value):
if raw_value is None:
raise RuntimeError("[NN_server/server.py] model classes are missing")
value = raw_value.strip()
if value.startswith('[') and value.endswith(']'):
value = value[1:-1]
classes = {}
for class_name in value.split(','):
class_name = class_name.strip()
if class_name:
classes[len(classes)] = class_name
if not classes:
raise RuntimeError("[NN_server/server.py] no classes parsed from NN_CLASSES_*")
return classes
def get_required_config(key):
value = config.get(key)
if value is None:
raise RuntimeError(f"[NN_server/server.py] missing required env key: {key}")
value = str(value).strip()
if not value:
raise RuntimeError(f"[NN_server/server.py] empty required env key: {key}")
return value
def get_optional_config(key, default=''):
value = config.get(key)
if value is None:
return default
return str(value).strip()
def build_model_specs():
build_func_name = get_optional_config('NN_BUILD_FUNC', 'build_func_ensemble')
pre_func_name = get_optional_config('NN_PRE_FUNC', 'pre_func_ensemble')
inference_func_name = get_optional_config('NN_INFERENCE_FUNC', 'inference_func_ensemble')
post_func_name = get_optional_config('NN_POST_FUNC', 'post_func_ensemble')
src_example = get_optional_config('NN_SRC_EXAMPLE', config['SRC_EXAMPLE'])
src_result = get_optional_config('NN_SRC_RESULT', config['SRC_RESULT'])
synthetic_examples = int(get_optional_config('NN_SYNTHETIC_EXAMPLES', '0'))
synthetic_mix_count = int(get_optional_config('NN_SYNTHETIC_MIX_COUNT', '1'))
src_dataset = get_optional_config('NN_SRC_DATASET', '')
specs = []
for freq in parse_freqs(config.get('NN_FREQS', config.get('FREQS', ''))):
module_name = get_required_config(f'NN_MODEL_{freq}')
weights = get_required_config(f'NN_WEIGHTS_{freq}')
classes = parse_classes(get_required_config(f'NN_CLASSES_{freq}'))
file_config = get_optional_config(f'NN_CONFIG_{freq}', get_optional_config('NN_CONFIG', ''))
specs.append({
'freq': freq,
'module_name': module_name,
'weights': weights,
'config': file_config,
'classes': classes,
'src_example': src_example,
'src_result': src_result,
'build_func_name': build_func_name,
'pre_func_name': pre_func_name,
'inference_func_name': inference_func_name,
'post_func_name': post_func_name,
'synthetic_examples': synthetic_examples,
'synthetic_mix_count': synthetic_mix_count,
'src_dataset': src_dataset,
})
return specs
if not config: if not config:
raise RuntimeError("[NN_server/server.py] .env was loaded but no keys were parsed") raise RuntimeError("[NN_server/server.py] .env was loaded but no keys were parsed")
if not any(is_model_config_key(key, value) for key, value in config.items()):
raise RuntimeError("[NN_server/server.py] no NN_* model entries configured")
logging.info("NN config loaded from %s", ROOT_ENV) logging.info("NN config loaded from %s", ROOT_ENV)
gen_server_ip = config['GENERAL_SERVER_IP'] gen_server_ip = config['GENERAL_SERVER_IP']
gen_server_port = config['GENERAL_SERVER_PORT'] gen_server_port = config['GENERAL_SERVER_PORT']
drone_streaks = {} drone_streaks = {}
MODEL_SPECS = build_model_specs()
INFERENCE_TELEMETRY_HOST = os.getenv('telemetry_host', '127.0.0.1')
INFERENCE_TELEMETRY_PORT = os.getenv('telemetry_port', '5020')
INFERENCE_TELEMETRY_ENDPOINT = os.getenv('telemetry_inference_endpoint', 'inference/result')
INFERENCE_TELEMETRY_TIMEOUT_SEC = float(os.getenv('telemetry_inference_timeout_sec', '0.30'))
def recreate_directory(path):
if os.path.isdir(path):
shutil.rmtree(path)
os.makedirs(path, exist_ok=True)
def get_result_dir():
if not MODEL_SPECS:
return ''
return MODEL_SPECS[0]['src_result']
def collect_inference_images(result_id):
result_dir = get_result_dir()
if not result_dir or not os.path.isdir(result_dir):
return []
needle = f"_inference_{result_id}_"
images = []
for name in sorted(os.listdir(result_dir)):
if needle in name and name.endswith('.png'):
images.append(name)
return images
def send_inference_result(payload):
try:
requests.post(
"http://{0}:{1}/{2}".format(
INFERENCE_TELEMETRY_HOST,
INFERENCE_TELEMETRY_PORT,
INFERENCE_TELEMETRY_ENDPOINT.lstrip('/'),
),
json=payload,
timeout=INFERENCE_TELEMETRY_TIMEOUT_SEC,
)
except Exception as exc:
print(str(exc))
def init_data_for_inference(): def init_data_for_inference():
try: try:
if os.path.isdir(config['SRC_RESULT']): if MODEL_SPECS:
shutil.rmtree(config['SRC_RESULT']) recreate_directory(MODEL_SPECS[0]['src_result'])
os.mkdir(config['SRC_RESULT']) recreate_directory(MODEL_SPECS[0]['src_example'])
if os.path.isdir(config['SRC_EXAMPLE']):
shutil.rmtree(config['SRC_EXAMPLE'])
os.mkdir(config['SRC_EXAMPLE'])
except Exception as exc: except Exception as exc:
print(str(exc)) print(str(exc))
print() print()
try: try:
global model_list global model_list
for key, value in config.items(): model_list.clear()
if is_model_config_key(key, value): for spec in MODEL_SPECS:
params = value.split(' && ') module = importlib.import_module('Models.' + spec['module_name'])
module = importlib.import_module('Models.' + params[4]) model = Model(
classes = {} freq=spec['freq'],
for value in params[9][1:-1].split(','): file_model=spec['weights'],
classes[len(classes)] = value file_config=spec['config'],
model = Model(file_model=params[0], file_config=params[1], src_example=params[2], src_result=params[3], src_example=spec['src_example'],
type_model=params[4], build_model_func=getattr(module, params[5]), src_result=spec['src_result'],
pre_func=getattr(module, params[6]), inference_func=getattr(module, params[7]), type_model=f"{spec['module_name']}@{spec['freq']}",
post_func=getattr(module, params[8]), classes=classes, number_synthetic_examples=int(params[10]), build_model_func=getattr(module, spec['build_func_name']),
number_src_data_for_one_synthetic_example=int(params[11]), path_to_src_dataset=params[12]) pre_func=getattr(module, spec['pre_func_name']),
inference_func=getattr(module, spec['inference_func_name']),
post_func=getattr(module, spec['post_func_name']),
classes=spec['classes'],
number_synthetic_examples=spec['synthetic_examples'],
number_src_data_for_one_synthetic_example=spec['synthetic_mix_count'],
path_to_src_dataset=spec['src_dataset'],
)
model_list.append(model) model_list.append(model)
# if key.startswith('ALG_'):
# params = config[key].split(' && ')
# module = importlib.import_module('Algorithms.' + params[2])
# classes = {}
# for value in params[6][1:-1].split(','):
# classes[len(classes)] = value
# alg = Algorithm(src_example=params[0], src_result=params[1], type_alg=params[2], pre_func=getattr(module, params[3]),
# inference_func=getattr(module, params[4]), post_func=getattr(module, params[5]), classes=classes,
# number_synthetic_examples=int(params[7]), number_src_data_for_one_synthetic_example=int(params[8]), path_to_src_dataset=params[9])
# alg_list.append(alg)
except Exception as exc: except Exception as exc:
print(str(exc)) print(str(exc))
print() print()
@ -142,44 +255,74 @@ def run_example():
print(str(exc)) print(str(exc))
def find_model_for_freq(freq):
for model in model_list:
if model.get_freq() == freq:
return model
return None
@app.route('/receive_data', methods=['POST']) @app.route('/receive_data', methods=['POST'])
def receive_data(): def receive_data():
try: try:
print() print()
data = json.loads(request.json) data = request.json
if isinstance(data, str):
data = json.loads(data)
print('#' * 100) print('#' * 100)
print('Получен пакет ' + str(Model.get_ind_inference())) print('Получен пакет ' + str(Model.get_ind_inference()))
result_id = Model.get_ind_inference()
freq = int(data['freq']) freq = int(data['freq'])
print('Частота: ' + str(freq)) print('Частота: ' + str(freq))
# print('Канал: ' + str(data['channel']))
result_msg = {} result_msg = {}
data_to_send = {} data_to_send = {}
prediction_list = [] prediction_list = []
#print(model_list) model = find_model_for_freq(freq)
for model in model_list: if model is None:
#print(str(freq)) raise RuntimeError(f"No NN model configured for freq={freq}")
#print(model.get_model_name())
if str(freq) in model.get_model_name():
print('-' * 100) print('-' * 100)
print(str(model)) print(str(model))
result_msg[str(model.get_model_name())] = {'freq': freq} result_msg[str(model.get_model_name())] = {'freq': freq}
prediction, probability = model.get_inference([np.asarray(data['data_real'], dtype=np.float32), np.asarray(data['data_imag'], dtype=np.float32)]) inference_result = model.get_inference([
np.asarray(data['data_real'], dtype=np.float32),
np.asarray(data['data_imag'], dtype=np.float32),
])
if inference_result is None:
raise RuntimeError(f"Inference failed for {model.get_model_name()}")
prediction, probability = inference_result[:2]
drone_probability = float(probability) if prediction == "drone" else 0.0
result_msg[str(model.get_model_name())]['prediction'] = prediction result_msg[str(model.get_model_name())]['prediction'] = prediction
result_msg[str(model.get_model_name())]['probability'] = str(probability) result_msg[str(model.get_model_name())]['probability'] = str(probability)
result_msg[str(model.get_model_name())]['drone_probability'] = str(drone_probability)
result_msg[str(model.get_model_name())]['drone_threshold'] = str(get_required_drone_prob(freq))
prediction_list.append(prediction) prediction_list.append(prediction)
inference_event = {
'result_id': result_id,
'ts': time.time(),
'freq': str(freq),
'model': model.get_model_name(),
'prediction': prediction,
'probability': float(probability),
'drone_probability': drone_probability,
'drone_threshold': str(get_required_drone_prob(freq)),
'images': collect_inference_images(result_id),
}
send_inference_result(inference_event)
print('-' * 100) print('-' * 100)
print() print()
try: try:
result = update_drone_streak(freq, prediction_list[0]) result = update_drone_streak(freq, prediction, drone_probability)
data_to_send = { data_to_send = {
'freq': str(freq), 'freq': str(freq),
'amplitude': result 'amplitude': result,
#'triggered': False if result < 7 else True,
#'light_len': result
} }
response = requests.post("http://{0}:{1}/process_data".format(gen_server_ip, gen_server_port), json=data_to_send) response = requests.post(
"http://{0}:{1}/process_data".format(gen_server_ip, gen_server_port),
json=data_to_send,
)
if response.status_code == 200: if response.status_code == 200:
print("Данные успешно отправлены!") print("Данные успешно отправлены!")
print("Частота: " + str(freq)) print("Частота: " + str(freq))
@ -188,7 +331,6 @@ def receive_data():
print("Ошибка при отправке данных: ", response.status_code) print("Ошибка при отправке данных: ", response.status_code)
except Exception as exc: except Exception as exc:
print(str(exc)) print(str(exc))
break
Model.get_inc_ind_inference() Model.get_inc_ind_inference()
print() print()
@ -197,11 +339,13 @@ def receive_data():
for alg in alg_list: for alg in alg_list:
print('-' * 100) print('-' * 100)
print(str(alg)) print(str(alg))
alg.get_inference([np.asarray(data['data_real'], dtype=np.float32), np.asarray(data['data_imag'], dtype=np.float32)]) alg.get_inference([
np.asarray(data['data_real'], dtype=np.float32),
np.asarray(data['data_imag'], dtype=np.float32),
])
print('-' * 100) print('-' * 100)
print() print()
#Algorithm.get_inc_ind_inference()
print() print()
print('#' * 100) print('#' * 100)
@ -214,128 +358,6 @@ def receive_data():
print(str(exc)) print(str(exc))
'''
def run_flask():
app.run(host=config['SERVER_IP'], port=int(config['SERVER_PORT']))
async def process_tasks():
workers = [asyncio.create_task(worker(queue=queue, semaphore=semaphore)) for _ in range(2)]
await asyncio.gather(*workers)
async def main():
asyncio.create_task(process_tasks())
flask_thread = threading.Thread(target=run_flask)
flask_thread.start()
while True:
if queue.qsize() <= 1:
asyncio.create_task(process_tasks())
await asyncio.sleep(1)
@app.route('/receive_data', methods=['POST'])
def add_task():
queue_size = queue.qsize()
if queue_size > 1:
return {}
print()
data = json.loads(request.json)
print('#' * 100)
print('Получен пакет ' + str(Model.get_ind_inference()))
freq = int(data['freq'])
print('Частота ' + str(freq))
result_msg = {}
for model in model_list:
if str(freq) in model.get_model_name():
print('-' * 100)
print(str(model))
result_msg[str(model.get_model_name())] = {'freq': freq}
asyncio.run_coroutine_threadsafe(queue.put({'freq': freq, 'model': model, 'data': data}), loop)
do_inference(model=model, data=data, freq=freq)
break
del data
gc.collect()
return jsonify(result_msg)
async def worker(queue, semaphore):
while True:
task = await queue.get()
if task is None:
break
async with semaphore:
try:
await do_inference(model=task['model'], data=task['data'], freq=task['freq'])
except Exception as e:
print(str(e))
print(results)
queue.task_done()
async def do_inference(model=None, data=None, freq=0):
prediction_list = []
print("Длина очереди" + str(queue.qsize()))
inference(model=model, data=data, freq=freq)
try:
results = []
for pred in prediction_list:
if pred[1] == 'drone':
results.append([pred[0],8])
else:
results.append([pred[0],0])
for result in results:
try:
data_to_send={
'freq': result[0],
'amplitude': result[1],
'triggered': False if result[1] < 7 else True,
'light_len': result[1]
}
response = requests.post("http://{0}:{1}/process_data".format(gen_server_ip, gen_server_port), json=data_to_send)
await response.text
if response.status_code == 200:
print("Данные успешно отправлены!")
print("Отправлено светодиодов: " + str(data_to_send['light_len']))
else:
print("Ошибка при отправке данных: ", response.status_code)
except Exception as exc:
print(str(exc))
except Exception as exc:
print(str(exc))
Model.get_inc_ind_inference()
print()
print('#' * 100)
del data
gc.collect()
def inference(model=None, data=None, freq=0):
prediction, probability = model.get_inference([np.asarray(data['data_real'], dtype=np.float32), np.asarray(data['data_imag'], dtype=np.float32)])
result_msg[str(model.get_model_name())]['prediction'] = prediction
result_msg[str(model.get_model_name())]['probability'] = str(probability)
queue_size = queue.qsize()
print(queue_size)
prediction_list.append([freq, prediction])
print('-' * 100)
print()
if __name__ == '__main__':
init_data_for_inference()
#asyncio.run(main)
loop.run_until_complete(main())
'''
def run_flask(): def run_flask():
print(config['SERVER_IP']) print(config['SERVER_IP'])
app.run(host=config['SERVER_IP'], port=int(config['SERVER_PORT'])) app.run(host=config['SERVER_IP'], port=int(config['SERVER_PORT']))
@ -346,5 +368,3 @@ if __name__ == '__main__':
flask_thread = threading.Thread(target=run_flask) flask_thread = threading.Thread(target=run_flask)
flask_thread.start() flask_thread.start()
#app.run(host=config['SERVER_IP'], port=int(config['SERVER_PORT']))

@ -6,25 +6,82 @@ SCRIPT_OWNER="${CAPTURE_USER:-$(stat -c %U "$SCRIPT_DIR")}"
SCRIPT_OWNER_HOME="$(getent passwd "$SCRIPT_OWNER" | cut -d: -f6)" SCRIPT_OWNER_HOME="$(getent passwd "$SCRIPT_OWNER" | cut -d: -f6)"
cd "$SCRIPT_DIR" cd "$SCRIPT_DIR"
source "$SCRIPT_DIR/.env" ENV_FILE="$SCRIPT_DIR/.env"
declare -A DOTENV_VALUES
trim_whitespace() {
local value="$1"
value="${value#"${value%%[![:space:]]*}"}"
value="${value%"${value##*[![:space:]]}"}"
printf '%s' "$value"
}
load_env_file() {
local line key value quote_char
if [[ ! -f "$ENV_FILE" ]]; then
echo "Не найден .env: $ENV_FILE" >&2
exit 1
fi
while IFS= read -r line || [[ -n "$line" ]]; do
line="${line%$'\r'}"
line="$(trim_whitespace "$line")"
[[ -z "$line" || "$line" == \#* ]] && continue
if [[ "$line" == export\ * ]]; then
line="${line#export }"
fi
[[ "$line" == *=* ]] || continue
key="$(trim_whitespace "${line%%=*}")"
value="$(trim_whitespace "${line#*=}")"
if [[ ${#value} -ge 2 ]]; then
quote_char="${value:0:1}"
if [[ ( "$quote_char" == "'" || "$quote_char" == '"' ) && "${value: -1}" == "$quote_char" ]]; then
value="${value:1:${#value}-2}"
fi
fi
DOTENV_VALUES["$key"]="$value"
done < "$ENV_FILE"
}
get_env_setting() {
local key="$1"
local default="${2-}"
if [[ -n "${!key+x}" ]]; then
printf '%s' "${!key}"
elif [[ -n "${DOTENV_VALUES[$key]+x}" ]]; then
printf '%s' "${DOTENV_VALUES[$key]}"
else
printf '%s' "$default"
fi
}
load_env_file
############################ ############################
# НАСТРОЙКИ # НАСТРОЙКИ
############################ ############################
BASE_DIR="${CAPTURE_BASE_DIR:-${SCRIPT_OWNER_HOME}/dataset/noise}" BASE_DIR="$(get_env_setting CAPTURE_BASE_DIR "${SCRIPT_OWNER_HOME}/dataset/noise")"
# Путь к python из venv # Путь к python из venv
PYTHON_BIN="${PYTHON_BIN:-$SCRIPT_DIR/.venv-sdr/bin/python}" PYTHON_BIN="$(get_env_setting PYTHON_BIN "$SCRIPT_DIR/.venv-sdr/bin/python")"
# Путь к headless скрипту # Путь к headless скрипту
SCRIPT_PATH="${SCRIPT_PATH:-$SCRIPT_DIR/scripts_nn/data_saver_headless.py}" SCRIPT_PATH="$(get_env_setting SCRIPT_PATH "$SCRIPT_DIR/scripts_nn/data_saver_headless.py")"
RUN_ONCE="${RUN_ONCE:-0}" RUN_ONCE="$(get_env_setting RUN_ONCE "0")"
CAPTURE_LOG_FILE="${CAPTURE_LOG_FILE:-$BASE_DIR/capture_hourly.log}" CAPTURE_LOG_FILE="$(get_env_setting CAPTURE_LOG_FILE "$BASE_DIR/capture_hourly.log")"
SYSTEMCTL_BIN=(systemctl) SYSTEMCTL_BIN=(systemctl)
CURRENT_CAPTURE_PID="" CURRENT_CAPTURE_PID=""
CURRENT_SERVICE_UNIT="" CURRENT_SERVICE_UNITS=()
@ -54,32 +111,32 @@ declare -A SERIAL
declare -A FREQ_HZ declare -A FREQ_HZ
declare -A SERVICE_UNIT declare -A SERVICE_UNIT
SERIAL[433]="$hack_433" SERIAL[433]="$(get_env_setting hack_433)"
FREQ_HZ[433]="433000000" FREQ_HZ[433]="433000000"
SERIAL[750]="$hack_750" SERIAL[750]="$(get_env_setting hack_750)"
FREQ_HZ[750]="750000000" FREQ_HZ[750]="750000000"
SERIAL[915]="$hack_915" SERIAL[915]="$(get_env_setting hack_915)"
FREQ_HZ[915]="915000000" FREQ_HZ[915]="915000000"
SERIAL[1200]="$hack_1200" SERIAL[1200]="$(get_env_setting hack_1200)"
FREQ_HZ[1200]="1200000000" FREQ_HZ[1200]="1200000000"
SERIAL[2400]="$hack_2400" SERIAL[2400]="$(get_env_setting hack_2400)"
FREQ_HZ[2400]="2400000000" FREQ_HZ[2400]="2400000000"
SERIAL[3300]="$hack_3300" SERIAL[3300]="$(get_env_setting hack_3300)"
FREQ_HZ[3300]="3300000000" FREQ_HZ[3300]="3300000000"
SERIAL[4500]="$hack_4500" SERIAL[4500]="$(get_env_setting hack_4500)"
FREQ_HZ[4500]="4500000000" FREQ_HZ[4500]="4500000000"
SERIAL[5200]="$hack_5200" SERIAL[5200]="$(get_env_setting hack_5200)"
FREQ_HZ[5200]="5200000000" FREQ_HZ[5200]="5200000000"
SERIAL[5800]="$hack_5800" SERIAL[5800]="$(get_env_setting hack_5800)"
FREQ_HZ[5800]="5800000000" FREQ_HZ[5800]="5800000000"
SERVICE_UNIT[433]="dronedetector-sdr-433.service" SERVICE_UNIT[433]="dronedetector-sdr-433.service"
@ -109,33 +166,69 @@ service_exists() {
systemctl_run show "$unit" >/dev/null 2>&1 systemctl_run show "$unit" >/dev/null 2>&1
} }
service_is_active() {
local unit="$1"
systemctl_run is-active --quiet "$unit"
}
remember_service_unit() {
local unit="$1"
local existing
for existing in "${CURRENT_SERVICE_UNITS[@]}"; do
if [[ "$existing" == "$unit" ]]; then
return 0
fi
done
CURRENT_SERVICE_UNITS+=("$unit")
}
stop_band_service() { stop_band_service() {
local band="$1" local band="$1"
local unit="${SERVICE_UNIT[$band]:-}" local serial="${SERIAL[$band]:-}"
local other_band unit other_serial
if [[ -z "$unit" ]]; then if [[ -z "$serial" ]]; then
log "Для band=$band не найден service unit" log "Для band=$band пустой serial, пропускаю stop/start сервисов"
return 0 return 0
fi fi
CURRENT_SERVICE_UNITS=()
for other_band in "${!SERVICE_UNIT[@]}"; do
unit="${SERVICE_UNIT[$other_band]:-}"
other_serial="${SERIAL[$other_band]:-}"
if [[ -z "$unit" || -z "$other_serial" || "$other_serial" != "$serial" ]]; then
continue
fi
if ! service_exists "$unit"; then if ! service_exists "$unit"; then
log "Service unit $unit не установлен, пропускаю stop/start" log "Service unit $unit не установлен, пропускаю"
return 0 continue
fi
if service_is_active "$unit"; then
remember_service_unit "$unit"
fi fi
log "Останавливаю service $unit перед записью band=$band" log "Останавливаю service $unit перед записью band=$band"
systemctl_run stop "$unit" systemctl_run stop "$unit"
CURRENT_SERVICE_UNIT="$unit" done
} }
start_current_service() { start_current_service() {
if [[ -z "$CURRENT_SERVICE_UNIT" ]]; then local unit
if [[ "${#CURRENT_SERVICE_UNITS[@]}" -eq 0 ]]; then
return 0 return 0
fi fi
log "Запускаю service $CURRENT_SERVICE_UNIT после записи" for unit in "${CURRENT_SERVICE_UNITS[@]}"; do
systemctl_run start "$CURRENT_SERVICE_UNIT" log "Запускаю service $unit после записи"
CURRENT_SERVICE_UNIT="" systemctl_run start "$unit"
done
CURRENT_SERVICE_UNITS=()
} }
cleanup_capture() { cleanup_capture() {
@ -146,10 +239,13 @@ cleanup_capture() {
fi fi
CURRENT_CAPTURE_PID="" CURRENT_CAPTURE_PID=""
if [[ -n "$CURRENT_SERVICE_UNIT" ]]; then if [[ "${#CURRENT_SERVICE_UNITS[@]}" -gt 0 ]]; then
log "Восстанавливаю service $CURRENT_SERVICE_UNIT при завершении скрипта" local unit
systemctl_run start "$CURRENT_SERVICE_UNIT" || true for unit in "${CURRENT_SERVICE_UNITS[@]}"; do
CURRENT_SERVICE_UNIT="" log "Восстанавливаю service $unit при завершении скрипта"
systemctl_run start "$unit" || true
done
CURRENT_SERVICE_UNITS=()
fi fi
} }
@ -200,10 +296,13 @@ ensure_requirements() {
mkdir -p "$BASE_DIR" mkdir -p "$BASE_DIR"
mkdir -p "$(dirname "$CAPTURE_LOG_FILE")" mkdir -p "$(dirname "$CAPTURE_LOG_FILE")"
if [[ -n "${CAPTURE_ORDER:-}" ]]; then local capture_order
capture_order="$(get_env_setting CAPTURE_ORDER)"
if [[ -n "$capture_order" ]]; then
ORDER=() ORDER=()
local band local band
for band in ${CAPTURE_ORDER//,/ }; do for band in ${capture_order//,/ }; do
ORDER+=("$band") ORDER+=("$band")
done done
fi fi

@ -1,3 +0,0 @@
SHARED_VECTOR_LEN = 4096
SHARED_868_ADDR = 'tcp://127.0.0.1:35068'
SHARED_915_ADDR = 'tcp://127.0.0.1:35069'

@ -97,6 +97,7 @@ services:
- ../../.env:/app/.env:ro - ../../.env:/app/.env:ro
- ../../telemetry:/app/telemetry - ../../telemetry:/app/telemetry
- ../../common:/app/common - ../../common:/app/common
- ../../NN_server/result:/app/inference_result:ro
networks: networks:
- dronedetector-net - dronedetector-net

@ -28,7 +28,7 @@ run_as_root() {
preflight() { preflight() {
[[ -f "${PROJECT_ROOT}/deploy/requirements/nn_gpu_pinned.txt" ]] || die "Missing deploy/requirements/nn_gpu_pinned.txt" [[ -f "${PROJECT_ROOT}/deploy/requirements/nn_gpu_pinned.txt" ]] || die "Missing deploy/requirements/nn_gpu_pinned.txt"
[[ -f "${PROJECT_ROOT}/requirements-train.txt" ]] || die "Missing train_scripts/requirements-train.txt" [[ -f "${PROJECT_ROOT}/train_scripts/requirements-train.txt" ]] || die "Missing train_scripts/requirements-train.txt"
[[ -f "${PROJECT_ROOT}/torchsig/pyproject.toml" ]] || die "Missing local torchsig package" [[ -f "${PROJECT_ROOT}/torchsig/pyproject.toml" ]] || die "Missing local torchsig package"
command -v "${PYTHON_BIN}" >/dev/null 2>&1 || die "${PYTHON_BIN} not found" command -v "${PYTHON_BIN}" >/dev/null 2>&1 || die "${PYTHON_BIN} not found"
} }

@ -21,7 +21,6 @@ server_port_2 = os.getenv('SERVER_PORT_2')
PARAMS = {'split_size': 400_000, 'point_amount': 100_000} PARAMS = {'split_size': 400_000, 'point_amount': 100_000}
PARAMS['show_amount'] = 0.8 * PARAMS['point_amount'] PARAMS['show_amount'] = 0.8 * PARAMS['point_amount']
token = 0 token = 0
channel = 1
flag = 0 flag = 0
############################## ##############################
@ -30,6 +29,7 @@ flag = 0
f_base = 1.1e9 f_base = 1.1e9
f_step = 20e6 f_step = 20e6
f_roof = 1.3e9 f_roof = 1.3e9
channel = f_base
############################## ##############################
# Variables # Variables
############################## ##############################
@ -96,7 +96,7 @@ def work(lvl):
if f >= f_roof: if f >= f_roof:
f = f_base f = f_base
signal_arr = [] signal_arr = []
channel = 1 channel = f
return f, EOCF return f, EOCF
else: else:
if flag == 0 and len(signal_arr) >= PARAMS['point_amount']: if flag == 0 and len(signal_arr) >= PARAMS['point_amount']:
@ -104,11 +104,11 @@ def work(lvl):
signal_arr = [] signal_arr = []
if flag == 0: if flag == 0:
f += f_step f += f_step
channel += 1 channel = f
if len(signal_arr) >= PARAMS['split_size']: if len(signal_arr) >= PARAMS['split_size']:
send_data(signal_arr[:PARAMS['split_size']]) send_data(signal_arr[:PARAMS['split_size']])
flag = 0 flag = 0
signal_arr = [] signal_arr = []
channel += 1
f += f_step f += f_step
channel = f
return f, EOCF return f, EOCF

@ -22,7 +22,6 @@ server_port_2 = os.getenv('SERVER_PORT_2')
PARAMS = {'split_size': 400_000, 'point_amount': 100_000} PARAMS = {'split_size': 400_000, 'point_amount': 100_000}
PARAMS['show_amount'] = 0.8 * PARAMS['point_amount'] PARAMS['show_amount'] = 0.8 * PARAMS['point_amount']
token = 0 token = 0
channel = 1
flag = 0 flag = 0
############################## ##############################
@ -31,6 +30,7 @@ flag = 0
f_base = 2.4e9 f_base = 2.4e9
f_step = 20e6 f_step = 20e6
f_roof = 2.5e9 f_roof = 2.5e9
channel = f_base
############################## ##############################
# Variables # Variables
############################## ##############################
@ -97,7 +97,7 @@ def work(lvl):
if f >= f_roof: if f >= f_roof:
f = f_base f = f_base
signal_arr = [] signal_arr = []
channel = 1 channel = f
return f, EOCF return f, EOCF
else: else:
if flag == 0 and len(signal_arr) >= PARAMS['point_amount']: if flag == 0 and len(signal_arr) >= PARAMS['point_amount']:
@ -105,11 +105,11 @@ def work(lvl):
signal_arr = [] signal_arr = []
if flag == 0: if flag == 0:
f += f_step f += f_step
channel += 1 channel = f
if len(signal_arr) >= PARAMS['split_size']: if len(signal_arr) >= PARAMS['split_size']:
send_data(signal_arr[:PARAMS['split_size']]) send_data(signal_arr[:PARAMS['split_size']])
flag = 0 flag = 0
signal_arr = [] signal_arr = []
channel += 1 channel = f
f += f_step f += f_step
return f, EOCF return f, EOCF

@ -22,7 +22,6 @@ server_port_2 = os.getenv('SERVER_PORT_2')
PARAMS = {'split_size': 400_000, 'point_amount': 100_000} PARAMS = {'split_size': 400_000, 'point_amount': 100_000}
PARAMS['show_amount'] = 0.8 * PARAMS['point_amount'] PARAMS['show_amount'] = 0.8 * PARAMS['point_amount']
token = 0 token = 0
channel = 1
flag = 0 flag = 0
############################## ##############################
@ -31,6 +30,7 @@ flag = 0
f_base = 5.9e9 f_base = 5.9e9
f_step = 20e6 f_step = 20e6
f_roof = 5.7e9 f_roof = 5.7e9
channel = f_base
############################## ##############################
# Variables # Variables
############################## ##############################
@ -97,7 +97,7 @@ def work(lvl):
if f >= f_roof: if f >= f_roof:
f = f_base f = f_base
signal_arr = [] signal_arr = []
channel = 1 channel = f
return f, EOCF return f, EOCF
else: else:
if flag == 0 and len(signal_arr) >= PARAMS['point_amount']: if flag == 0 and len(signal_arr) >= PARAMS['point_amount']:
@ -105,11 +105,11 @@ def work(lvl):
signal_arr = [] signal_arr = []
if flag == 0: if flag == 0:
f += f_step f += f_step
channel += 1 channel = f
if len(signal_arr) >= PARAMS['split_size']: if len(signal_arr) >= PARAMS['split_size']:
send_data(signal_arr[:PARAMS['split_size']]) send_data(signal_arr[:PARAMS['split_size']])
flag = 0 flag = 0
signal_arr = [] signal_arr = []
channel += 1 channel = f
f += f_step f += f_step
return f, EOCF return f, EOCF

@ -21,12 +21,12 @@ server_port_2 = os.getenv('SERVER_PORT_2')
PARAMS = {'split_size': 400_000, 'point_amount': 100_000} PARAMS = {'split_size': 400_000, 'point_amount': 100_000}
PARAMS['show_amount'] = int(0.8 * PARAMS['point_amount']) PARAMS['show_amount'] = int(0.8 * PARAMS['point_amount'])
token = 0 token = 0
channel = 1
flag = 0 flag = 0
f_base = 0.91e9 f_base = 0.91e9
f_step = 20e6 f_step = 20e6
f_roof = 0.98e9 f_roof = 0.98e9
channel = f_base
f = f_base f = f_base
EOCF = 0 EOCF = 0
@ -85,11 +85,11 @@ def advance_freq():
next_freq = f + f_step next_freq = f + f_step
if next_freq >= f_roof: if next_freq >= f_roof:
f = f_base f = f_base
channel = 1 channel = f
return f, True return f, True
f = next_freq f = next_freq
channel += 1 channel = f
return f, False return f, False

@ -1,88 +1,168 @@
from gnuradio import blocks, gr, zeromq from gnuradio import blocks, gr
import signal
import sys import sys
import threading import signal
import time
import compose_send_data_915 as my_freq import compose_send_data_915 as my_freq
import osmosdr
from common.runtime import load_root_env import time
from common.shared_stream_addrs import SHARED_915_ADDR, SHARED_VECTOR_LEN import threading
import subprocess
import os
from common.runtime import load_root_env, resolve_hackrf_index
load_root_env(__file__) load_root_env(__file__)
def get_hack_id():
return resolve_hackrf_index('HACKID_915', 'orange_scripts/main_915.py')
serial_number = os.getenv('HACKID_1200')
pos = None
output = []
try:
command = 'lsusb -v -d 1d50:6089 | grep iSerial'
output.append(subprocess.check_output(command, shell=True, text=True))
except subprocess.CalledProcessError as e:
print(f"Команда завершилась с кодом возврата {e.returncode}")
print(e)
print(output)
output_lines = output[0].strip().split('\n')
print(output_lines)
serial_numbers = [line.split()[-1] for line in output_lines]
print(serial_numbers)
for i, number in enumerate(serial_numbers):
if number == serial_number:
id = i
break
if id is not None:
print('HackId is: {0}'.format(id))
return str(id)
else:
print('Такого хака нет!')
class get_center_freq(gr.top_block): class get_center_freq(gr.top_block):
def __init__(self): def __init__(self):
gr.top_block.__init__(self, 'get_center_freq') gr.top_block.__init__(self, "get_center_freq")
self.prob_freq = 0 ##################################################
self.poll_rate = 10000 # Variables
self.vector_len = SHARED_VECTOR_LEN ##################################################
self.center_freq = 0 self.prob_freq = prob_freq = 0
self.shared_addr = SHARED_915_ADDR self.top_peaks_amount = top_peaks_amount = 20
self._stop_polling = threading.Event() self.samp_rate = samp_rate = 20e6
self._prob_freq_thread = None self.poll_rate = poll_rate = 10000
self.num_points = num_points = 8192
self.probSigVec = blocks.probe_signal_vc(self.vector_len) self.flag = flag = 1
self.shared_source_0 = zeromq.pull_source( self.decimation = decimation = 1
gr.sizeof_gr_complex, self.center_freq = center_freq = my_freq.work(prob_freq)[0]
self.vector_len,
self.shared_addr, ##################################################
100, # Blocks
False, ##################################################
-1, self.probSigVec = blocks.probe_signal_vc(4096)
False, self.rtlsdr_source_0 = osmosdr.source(
args="numchan=" + str(1) + " " + 'hackrf=' + get_hack_id()
) )
self.connect((self.shared_source_0, 0), (self.probSigVec, 0)) self.rtlsdr_source_0.set_time_unknown_pps(osmosdr.time_spec_t())
self.rtlsdr_source_0.set_sample_rate(samp_rate)
def start_polling(self): self.rtlsdr_source_0.set_center_freq(center_freq, 0)
if self._prob_freq_thread is not None: self.rtlsdr_source_0.set_freq_corr(0, 0)
return self.rtlsdr_source_0.set_gain(16, 0)
self.rtlsdr_source_0.set_if_gain(16, 0)
self.rtlsdr_source_0.set_bb_gain(0, 0)
self.rtlsdr_source_0.set_antenna('', 0)
self.rtlsdr_source_0.set_bandwidth(0, 0)
self.rtlsdr_source_0.set_min_output_buffer(4096)
def _prob_freq_probe(): def _prob_freq_probe():
while not self._stop_polling.is_set(): while True:
self.set_prob_freq(self.probSigVec.level())
time.sleep(1.0 / self.poll_rate) val = self.probSigVec.level()
try:
self.set_prob_freq(val)
except AttributeError:
pass
time.sleep(1.0 / (poll_rate))
_prob_freq_thread = threading.Thread(target=_prob_freq_probe)
_prob_freq_thread.daemon = True
_prob_freq_thread.start()
self.blocks_stream_to_vector_1 = blocks.stream_to_vector(gr.sizeof_gr_complex*1, 4096)
self._prob_freq_thread = threading.Thread(target=_prob_freq_probe, daemon=True)
self._prob_freq_thread.start() ##################################################
# Connections
##################################################
self.connect((self.blocks_stream_to_vector_1, 0), (self.probSigVec, 0))
self.connect((self.rtlsdr_source_0, 0), (self.blocks_stream_to_vector_1, 0))
def get_prob_freq(self): def get_prob_freq(self):
return self.prob_freq return self.prob_freq
def set_prob_freq(self, prob_freq): def set_prob_freq(self, prob_freq):
self.prob_freq = prob_freq self.prob_freq = prob_freq
self.center_freq = my_freq.work(self.prob_freq)[0] self.set_center_freq(my_freq.work(self.prob_freq)[0])
def get_top_peaks_amount(self):
return self.top_peaks_amount
def set_top_peaks_amount(self, top_peaks_amount):
self.top_peaks_amount = top_peaks_amount
def get_samp_rate(self):
return self.samp_rate
def set_samp_rate(self, samp_rate):
self.samp_rate = samp_rate
self.rtlsdr_source_0.set_sample_rate(self.samp_rate)
def get_poll_rate(self):
return self.poll_rate
def set_poll_rate(self, poll_rate):
self.poll_rate = poll_rate
def get_num_points(self):
return self.num_points
def set_num_points(self, num_points):
self.num_points = num_points
def get_flag(self):
return self.flag
def set_flag(self, flag):
self.flag = flag
def get_decimation(self):
return self.decimation
def set_decimation(self, decimation):
self.decimation = decimation
def get_center_freq(self): def get_center_freq(self):
return self.center_freq return self.center_freq
def set_center_freq(self, center_freq): def set_center_freq(self, center_freq):
self.center_freq = center_freq self.center_freq = center_freq
self.rtlsdr_source_0.set_center_freq(self.center_freq, 0)
def close(self):
self._stop_polling.set()
self.stop()
self.wait()
def main(top_block_cls=get_center_freq, options=None): def main(top_block_cls=get_center_freq, options=None):
time.sleep(3) time.sleep(3)
tb = top_block_cls() tb = top_block_cls()
def sig_handler(sig=None, frame=None): def sig_handler(sig=None, frame=None):
tb.close() tb.stop()
tb.wait()
sys.exit(0) sys.exit(0)
signal.signal(signal.SIGINT, sig_handler) signal.signal(signal.SIGINT, sig_handler)
signal.signal(signal.SIGTERM, sig_handler) signal.signal(signal.SIGTERM, sig_handler)
tb.start() tb.start()
tb.start_polling()
try: try:
print('shared_pull_addr:', SHARED_915_ADDR) input('Press Enter to quit: ')
except EOFError: except EOFError:
pass pass
tb.wait() tb.wait()

@ -24,6 +24,7 @@ def agregator(freq, alarm):
amplitude = 0 amplitude = 0
data = {"freq": freq, data = {"freq": freq,
#"channel": channel,
"amplitude": amplitude "amplitude": amplitude
} }
return data return data

@ -2,10 +2,11 @@ import asyncio
import os import os
import time import time
from collections import defaultdict, deque from collections import defaultdict, deque
from pathlib import Path
from typing import Any, Deque, Dict, List, Optional from typing import Any, Deque, Dict, List, Optional
from fastapi import FastAPI, Query, WebSocket, WebSocketDisconnect from fastapi import FastAPI, HTTPException, Query, WebSocket, WebSocketDisconnect
from fastapi.responses import HTMLResponse from fastapi.responses import FileResponse, HTMLResponse
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from common.runtime import load_root_env from common.runtime import load_root_env
@ -16,15 +17,24 @@ TELEMETRY_BIND_HOST = os.getenv('telemetry_bind_host', os.getenv('lochost', '0.0
TELEMETRY_BIND_PORT = int(os.getenv('telemetry_bind_port', os.getenv('telemetry_port', '5020'))) 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_HISTORY_SEC = int(float(os.getenv('telemetry_history_sec', '900')))
TELEMETRY_MAX_POINTS_PER_FREQ = int(os.getenv('telemetry_max_points_per_freq', '5000')) TELEMETRY_MAX_POINTS_PER_FREQ = int(os.getenv('telemetry_max_points_per_freq', '5000'))
INFERENCE_HISTORY_SEC = int(float(os.getenv('inference_history_sec', str(TELEMETRY_HISTORY_SEC))))
INFERENCE_MAX_RESULTS_PER_FREQ = int(os.getenv('inference_max_results_per_freq', '100'))
INFERENCE_RESULT_DIR = Path(os.getenv('inference_result_dir', '/app/inference_result')).resolve()
def _new_buffer() -> Deque[Dict[str, Any]]: def _new_buffer() -> Deque[Dict[str, Any]]:
return deque(maxlen=TELEMETRY_MAX_POINTS_PER_FREQ) return deque(maxlen=TELEMETRY_MAX_POINTS_PER_FREQ)
def _new_inference_buffer() -> Deque[Dict[str, Any]]:
return deque(maxlen=INFERENCE_MAX_RESULTS_PER_FREQ)
app = FastAPI(title='DroneDetector Telemetry Server') app = FastAPI(title='DroneDetector Telemetry Server')
_buffers: Dict[str, Deque[Dict[str, Any]]] = defaultdict(_new_buffer) _buffers: Dict[str, Deque[Dict[str, Any]]] = defaultdict(_new_buffer)
_ws_clients: List[WebSocket] = [] _ws_clients: List[WebSocket] = []
_inference_buffers: Dict[str, Deque[Dict[str, Any]]] = defaultdict(_new_inference_buffer)
_inference_ws_clients: List[WebSocket] = []
_state_lock = asyncio.Lock() _state_lock = asyncio.Lock()
@ -41,6 +51,18 @@ class TelemetryPoint(BaseModel):
alarm_channels: Optional[List[int]] = None alarm_channels: Optional[List[int]] = None
class InferenceResult(BaseModel):
result_id: int
ts: float = Field(default_factory=lambda: time.time())
freq: str
model: str
prediction: str
probability: float
drone_probability: Optional[float] = None
drone_threshold: Optional[str] = None
images: List[str] = Field(default_factory=list)
def _prune_freq_locked(freq: str, now_ts: float) -> None: def _prune_freq_locked(freq: str, now_ts: float) -> None:
cutoff = now_ts - TELEMETRY_HISTORY_SEC cutoff = now_ts - TELEMETRY_HISTORY_SEC
buf = _buffers[freq] buf = _buffers[freq]
@ -62,6 +84,40 @@ def _copy_series_locked(seconds: int, freq: Optional[str] = None) -> Dict[str, L
return series return series
def _prune_inference_freq_locked(freq: str, now_ts: float) -> None:
cutoff = now_ts - INFERENCE_HISTORY_SEC
buf = _inference_buffers[freq]
while buf and float(buf[0].get('ts', 0.0)) < cutoff:
buf.popleft()
def _copy_inference_series_locked(limit: int, freq: Optional[str] = None) -> Dict[str, List[Dict[str, Any]]]:
now_ts = time.time()
cutoff = now_ts - INFERENCE_HISTORY_SEC
def _slice(buf: Deque[Dict[str, Any]]) -> List[Dict[str, Any]]:
recent = [item for item in buf if float(item.get('ts', 0.0)) >= cutoff]
return recent[-limit:]
if freq is not None:
return {freq: _slice(_inference_buffers.get(freq, deque()))}
series: Dict[str, List[Dict[str, Any]]] = {}
for key, buf in _inference_buffers.items():
series[key] = _slice(buf)
return series
def _sanitize_image_names(names: List[str]) -> List[str]:
safe_names: List[str] = []
for name in names:
base = Path(str(name)).name
if not base or not base.endswith('.png'):
continue
safe_names.append(base)
return safe_names
async def _broadcast(message: Dict[str, Any]) -> None: async def _broadcast(message: Dict[str, Any]) -> None:
dead: List[WebSocket] = [] dead: List[WebSocket] = []
for ws in list(_ws_clients): for ws in list(_ws_clients):
@ -77,6 +133,21 @@ async def _broadcast(message: Dict[str, Any]) -> None:
_ws_clients.remove(ws) _ws_clients.remove(ws)
async def _broadcast_inference(message: Dict[str, Any]) -> None:
dead: List[WebSocket] = []
for ws in list(_inference_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 _inference_ws_clients:
_inference_ws_clients.remove(ws)
@app.post('/telemetry') @app.post('/telemetry')
async def ingest_telemetry(point: TelemetryPoint): async def ingest_telemetry(point: TelemetryPoint):
payload = point.model_dump() payload = point.model_dump()
@ -102,6 +173,31 @@ async def telemetry_history(
return {'seconds': seconds, 'series': series} return {'seconds': seconds, 'series': series}
@app.post('/inference/result')
async def ingest_inference_result(result: InferenceResult):
payload = result.model_dump()
payload['images'] = _sanitize_image_names(payload.get('images', []))
freq = str(payload['freq'])
now_ts = time.time()
async with _state_lock:
_inference_buffers[freq].append(payload)
_prune_inference_freq_locked(freq, now_ts)
await _broadcast_inference({'type': 'inference_result', 'data': payload})
return {'ok': True}
@app.get('/inference/history')
async def inference_history(
freq: Optional[str] = Query(default=None),
limit: int = Query(default=20, ge=1, le=200),
):
async with _state_lock:
series = _copy_inference_series_locked(limit=limit, freq=freq)
return {'limit': limit, 'series': series}
@app.websocket('/telemetry/ws') @app.websocket('/telemetry/ws')
async def telemetry_ws(websocket: WebSocket): async def telemetry_ws(websocket: WebSocket):
await websocket.accept() await websocket.accept()
@ -123,6 +219,26 @@ async def telemetry_ws(websocket: WebSocket):
_ws_clients.remove(websocket) _ws_clients.remove(websocket)
@app.websocket('/inference/ws')
async def inference_ws(websocket: WebSocket):
await websocket.accept()
async with _state_lock:
_inference_ws_clients.append(websocket)
snapshot = _copy_inference_series_locked(limit=20, freq=None)
await websocket.send_json({'type': 'snapshot', 'data': snapshot})
try:
while True:
await websocket.receive_text()
except WebSocketDisconnect:
pass
finally:
async with _state_lock:
if websocket in _inference_ws_clients:
_inference_ws_clients.remove(websocket)
MONITOR_HTML = """ MONITOR_HTML = """
<!doctype html> <!doctype html>
<html> <html>
@ -453,12 +569,294 @@ loadInitial().then(connectWs).catch((e) => {
""" """
INFERENCE_VIEWER_HTML = """
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>DroneDetector Inference Viewer</title>
<style>
:root {
--bg: #f4f6f8;
--card: #ffffff;
--line: #d7dde5;
--text: #1c232e;
--muted: #647084;
--ok: #16a34a;
--warn: #b45309;
--accent: #0f6fff;
}
* { box-sizing: border-box; }
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: 16px; }
.head { display: flex; justify-content: space-between; align-items: center; gap: 16px; margin-bottom: 16px; }
.meta { color: var(--muted); font-size: 13px; }
.actions { display: flex; gap: 8px; align-items: center; }
button { border: 1px solid var(--line); background: var(--card); color: var(--text); border-radius: 8px; padding: 8px 12px; cursor: pointer; }
button.primary { background: var(--accent); color: #fff; border-color: var(--accent); }
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(520px, 1fr)); gap: 14px; }
.card { background: var(--card); border: 1px solid var(--line); border-radius: 14px; padding: 14px; }
.card-head { display: flex; justify-content: space-between; align-items: start; gap: 12px; margin-bottom: 10px; }
.freq { font-size: 28px; font-weight: 700; }
.pill { display: inline-flex; align-items: center; gap: 6px; border-radius: 999px; padding: 4px 10px; font-size: 12px; }
.pill.live { background: #dcfce7; color: #166534; }
.pill.paused { background: #fef3c7; color: #92400e; }
.summary { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px 12px; margin-bottom: 12px; }
.summary-item { border: 1px solid var(--line); border-radius: 10px; padding: 8px 10px; }
.summary-label { color: var(--muted); font-size: 12px; margin-bottom: 4px; }
.summary-value { font-size: 14px; font-weight: 600; word-break: break-word; }
.images { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 10px; margin-bottom: 12px; }
.image-card { border: 1px solid var(--line); border-radius: 10px; overflow: hidden; background: #f8fafc; }
.image-card img { width: 100%; height: 180px; object-fit: contain; display: block; background: #fff; }
.image-card .cap { padding: 6px 8px; font-size: 12px; color: var(--muted); border-top: 1px solid var(--line); }
.history-title { font-size: 13px; font-weight: 600; margin-bottom: 8px; }
.history { display: flex; gap: 8px; overflow-x: auto; padding-bottom: 4px; }
.thumb { min-width: 110px; max-width: 110px; border: 1px solid var(--line); border-radius: 10px; background: #fff; padding: 6px; cursor: pointer; }
.thumb.active { border-color: var(--accent); box-shadow: inset 0 0 0 1px var(--accent); }
.thumb img { width: 100%; height: 70px; object-fit: contain; display: block; background: #f8fafc; border-radius: 6px; }
.thumb .t1 { font-size: 12px; font-weight: 600; margin-top: 6px; }
.thumb .t2 { font-size: 11px; color: var(--muted); }
.empty { color: var(--muted); font-size: 13px; padding: 16px 0; }
</style>
</head>
<body>
<div class="wrap">
<div class="head">
<div>
<h2 style="margin:0 0 4px;">DroneDetector Inference Viewer</h2>
<div class="meta">Latest inference card per frequency. Browser keeps last 20 results per frequency.</div>
</div>
<div class="actions">
<div class="meta" id="ws-status">connecting...</div>
<button id="pause-btn" class="primary">Pause updates</button>
</div>
</div>
<div class="grid" id="cards"></div>
</div>
<script>
const MAX_RESULTS = 20;
const state = {};
const selected = {};
let paused = false;
let wsKeepalive = null;
function freqSort(a, b) { return Number(a) - Number(b); }
function formatTs(ts) {
return new Date(Number(ts) * 1000).toLocaleString('ru-RU', { hour12: false });
}
function escapeHtml(value) {
return String(value ?? '').replaceAll('&', '&amp;').replaceAll('<', '&lt;').replaceAll('>', '&gt;').replaceAll('"', '&quot;');
}
function imageUrl(name) {
return `/inference/images/${encodeURIComponent(name)}`;
}
function primaryImage(result) {
return (result.images || [])[0] || '';
}
function trimResults(freq) {
const arr = state[freq] || [];
state[freq] = arr.slice(-MAX_RESULTS);
}
function ensureCard(freq) {
if (document.getElementById(`card-${freq}`)) return;
const card = document.createElement('div');
card.className = 'card';
card.id = `card-${freq}`;
card.innerHTML = `
<div class="card-head">
<div class="freq">${escapeHtml(freq)} MHz</div>
<div class="pill ${paused ? 'paused' : 'live'}" id="badge-${freq}">${paused ? 'paused' : 'live'}</div>
</div>
<div class="summary" id="summary-${freq}"></div>
<div class="images" id="images-${freq}"></div>
<div class="history-title">Recent results</div>
<div class="history" id="history-${freq}"></div>
`;
document.getElementById('cards').appendChild(card);
}
function renderSummary(freq, result) {
const el = document.getElementById(`summary-${freq}`);
el.innerHTML = `
<div class="summary-item"><div class="summary-label">Model</div><div class="summary-value">${escapeHtml(result.model)}</div></div>
<div class="summary-item"><div class="summary-label">Prediction</div><div class="summary-value">${escapeHtml(result.prediction)}</div></div>
<div class="summary-item"><div class="summary-label">Confidence</div><div class="summary-value">${Number(result.probability).toFixed(3)}</div></div>
<div class="summary-item"><div class="summary-label">Time</div><div class="summary-value">${escapeHtml(formatTs(result.ts))}</div></div>
`;
}
function renderImages(freq, result) {
const el = document.getElementById(`images-${freq}`);
const images = result.images || [];
if (!images.length) {
el.innerHTML = '<div class="empty">No images for this inference.</div>';
return;
}
el.innerHTML = images.map((name) => `
<div class="image-card">
<img loading="lazy" src="${imageUrl(name)}" alt="${escapeHtml(name)}" />
<div class="cap">${escapeHtml(name)}</div>
</div>
`).join('');
}
function renderHistory(freq) {
const el = document.getElementById(`history-${freq}`);
const results = state[freq] || [];
if (!results.length) {
el.innerHTML = '<div class="empty">No results yet.</div>';
return;
}
el.innerHTML = results.slice().reverse().map((result) => {
const active = String(selected[freq]) === String(result.result_id) ? 'active' : '';
const image = primaryImage(result);
return `
<button class="thumb ${active}" data-freq="${escapeHtml(freq)}" data-result-id="${result.result_id}">
${image ? `<img loading="lazy" src="${imageUrl(image)}" alt="${escapeHtml(result.prediction)}" />` : '<div class="empty" style="padding:20px 0;">no image</div>'}
<div class="t1">${escapeHtml(result.prediction)}</div>
<div class="t2">p=${Number(result.probability).toFixed(2)}</div>
</button>
`;
}).join('');
el.querySelectorAll('.thumb').forEach((node) => {
node.addEventListener('click', () => {
selected[freq] = Number(node.dataset.resultId);
render(freq);
});
});
}
function render(freq) {
ensureCard(freq);
trimResults(freq);
const badge = document.getElementById(`badge-${freq}`);
badge.textContent = paused ? 'paused' : 'live';
badge.className = `pill ${paused ? 'paused' : 'live'}`;
const results = state[freq] || [];
if (!results.length) {
document.getElementById(`summary-${freq}`).innerHTML = '<div class="empty">No inference results yet.</div>';
document.getElementById(`images-${freq}`).innerHTML = '';
document.getElementById(`history-${freq}`).innerHTML = '';
return;
}
const active = results.find((item) => String(item.result_id) === String(selected[freq])) || results[results.length - 1];
selected[freq] = active.result_id;
renderSummary(freq, active);
renderImages(freq, active);
renderHistory(freq);
}
function renderAll() {
Object.keys(state).sort(freqSort).forEach(render);
}
function applySnapshot(series) {
const next = {};
for (const [freq, results] of Object.entries(series || {})) {
next[String(freq)] = Array.isArray(results) ? results.slice(-MAX_RESULTS) : [];
}
for (const freq of Object.keys(next)) {
state[freq] = next[freq];
if (state[freq].length) {
selected[freq] = state[freq][state[freq].length - 1].result_id;
}
}
renderAll();
}
function ingestResult(result) {
const freq = String(result.freq);
if (!state[freq]) state[freq] = [];
state[freq].push(result);
trimResults(freq);
selected[freq] = result.result_id;
render(freq);
}
async function loadHistory() {
const res = await fetch(`/inference/history?limit=${MAX_RESULTS}`);
const payload = await res.json();
applySnapshot(payload.series || {});
}
function setPaused(nextPaused) {
paused = nextPaused;
const btn = document.getElementById('pause-btn');
btn.textContent = paused ? 'Resume updates' : 'Pause updates';
btn.className = paused ? '' : 'primary';
renderAll();
}
function connectWs() {
const proto = location.protocol === 'https:' ? 'wss' : 'ws';
const ws = new WebSocket(`${proto}://${location.host}/inference/ws`);
ws.onopen = () => {
document.getElementById('ws-status').textContent = paused ? 'ws connected (paused)' : 'ws connected';
if (wsKeepalive) clearInterval(wsKeepalive);
wsKeepalive = setInterval(() => {
if (ws.readyState === 1) ws.send('ping');
}, 20000);
};
ws.onmessage = (event) => {
const msg = JSON.parse(event.data);
if (msg.type === 'snapshot' && msg.data) {
if (!paused) applySnapshot(msg.data);
return;
}
if (msg.type !== 'inference_result' || paused) return;
ingestResult(msg.data);
};
ws.onclose = () => {
document.getElementById('ws-status').textContent = 'ws disconnected, retrying...';
if (wsKeepalive) clearInterval(wsKeepalive);
setTimeout(connectWs, 1500);
};
ws.onerror = () => {
document.getElementById('ws-status').textContent = 'ws error';
};
}
document.getElementById('pause-btn').addEventListener('click', async () => {
if (paused) {
setPaused(false);
document.getElementById('ws-status').textContent = 'syncing...';
try {
await loadHistory();
document.getElementById('ws-status').textContent = 'ws connected';
} catch (err) {
document.getElementById('ws-status').textContent = `history error: ${err}`;
}
} else {
setPaused(true);
document.getElementById('ws-status').textContent = 'ws connected (paused)';
}
});
loadHistory().then(connectWs).catch((err) => {
document.getElementById('ws-status').textContent = `init error: ${err}`;
connectWs();
});
</script>
</body>
</html>
"""
@app.get('/', response_class=HTMLResponse) @app.get('/', response_class=HTMLResponse)
@app.get('/monitor', response_class=HTMLResponse) @app.get('/monitor', response_class=HTMLResponse)
async def monitor_page(): async def monitor_page():
return HTMLResponse(content=MONITOR_HTML) return HTMLResponse(content=MONITOR_HTML)
@app.get('/inference-viewer', response_class=HTMLResponse)
async def inference_viewer_page():
return HTMLResponse(content=INFERENCE_VIEWER_HTML)
@app.get('/inference/images/{filename}')
async def inference_image(filename: str):
safe_name = Path(filename).name
if safe_name != filename:
raise HTTPException(status_code=404, detail='image not found')
image_path = (INFERENCE_RESULT_DIR / safe_name).resolve()
if image_path.parent != INFERENCE_RESULT_DIR or not image_path.is_file():
raise HTTPException(status_code=404, detail='image not found')
return FileResponse(image_path)
if __name__ == '__main__': if __name__ == '__main__':
import uvicorn import uvicorn

Loading…
Cancel
Save