G
2025-09-23 11:07:54 -04:00
|
|
|
from flask import Flask, render_template, send_file, request, jsonify
|
G
2026-04-24 09:51:23 -04:00
|
|
|
from flask_socketio import SocketIO
|
G
2025-09-23 11:07:54 -04:00
|
|
|
import numpy as np
|
G
2026-04-24 09:51:23 -04:00
|
|
|
import matplotlib
|
|
|
|
|
matplotlib.use('Agg')
|
G
2025-09-23 11:07:54 -04:00
|
|
|
import matplotlib.pyplot as plt
|
|
|
|
|
import matplotlib.ticker as ticker
|
|
|
|
|
import os
|
|
|
|
|
import threading
|
|
|
|
|
import time
|
|
|
|
|
import serial
|
G
2025-09-25 20:53:22 -04:00
|
|
|
import json
|
G
2026-04-24 09:51:23 -04:00
|
|
|
import base64
|
|
|
|
|
import io
|
|
|
|
|
import traceback
|
|
|
|
|
import socket
|
|
|
|
|
from collections import deque
|
G
2025-09-23 11:07:54 -04:00
|
|
|
|
|
|
|
|
app = Flask(__name__)
|
G
2026-04-24 09:51:23 -04:00
|
|
|
socketio = SocketIO(app, cors_allowed_origins="*")
|
G
2025-09-25 13:24:23 -04:00
|
|
|
PLOT_PATH = os.path.join(os.getcwd(), "plot.png")
|
|
|
|
|
|
G
2025-09-25 20:53:22 -04:00
|
|
|
# ----------------- Shared Config -----------------
|
|
|
|
|
config = {
|
|
|
|
|
"usrp_tx_gain": 60,
|
|
|
|
|
"usrp_rx_gain": 30,
|
|
|
|
|
"scm_tx_gain": 30,
|
|
|
|
|
"scm_rx_gain": 30,
|
|
|
|
|
"sample_rate": 23.04e6,
|
|
|
|
|
"window_ms": 20,
|
|
|
|
|
"center_freq": 3.415e9,
|
|
|
|
|
"NFFT": 1024,
|
G
2026-04-24 09:51:23 -04:00
|
|
|
"iq_port": 5588,
|
|
|
|
|
"streaming": False,
|
|
|
|
|
"packets_received": 0,
|
|
|
|
|
"iq_bandwidth_mbps": 0.0,
|
G
2025-09-25 20:53:22 -04:00
|
|
|
}
|
|
|
|
|
config_lock = threading.Lock()
|
|
|
|
|
|
|
|
|
|
usrp_tx_gain = config["usrp_tx_gain"]
|
|
|
|
|
usrp_rx_gain = config["usrp_rx_gain"]
|
|
|
|
|
scm_tx_gain = config["scm_tx_gain"]
|
|
|
|
|
scm_rx_gain = config["scm_rx_gain"]
|
|
|
|
|
|
|
|
|
|
plot_thread = None
|
G
2026-04-24 09:51:23 -04:00
|
|
|
rx_thread = None
|
G
2025-09-25 20:53:22 -04:00
|
|
|
stop_event = threading.Event()
|
|
|
|
|
pause_event = threading.Event()
|
G
2025-09-25 11:28:00 -04:00
|
|
|
|
G
2026-04-24 09:51:23 -04:00
|
|
|
latest_iq_data = None
|
|
|
|
|
latest_data_lock = threading.Lock()
|
|
|
|
|
|
|
|
|
|
iq_buffer = deque(maxlen=1)
|
|
|
|
|
iq_buffer_lock = threading.Lock()
|
|
|
|
|
|
|
|
|
|
udp_sock = None
|
|
|
|
|
udp_sock_lock = threading.Lock()
|
|
|
|
|
|
|
|
|
|
MAX_TIME_PLOT_POINTS = 5000
|
|
|
|
|
PLOT_REFRESH_SEC = 0.25
|
|
|
|
|
|
|
|
|
|
|
G
2025-09-23 11:07:54 -04:00
|
|
|
def connect_serial(port, baudrate=115200, timeout=1):
|
|
|
|
|
try:
|
G
2026-04-24 09:51:23 -04:00
|
|
|
return serial.Serial(
|
G
2025-09-23 11:07:54 -04:00
|
|
|
port=port,
|
|
|
|
|
baudrate=baudrate,
|
|
|
|
|
timeout=timeout,
|
|
|
|
|
bytesize=serial.EIGHTBITS,
|
|
|
|
|
parity=serial.PARITY_EVEN,
|
G
2026-04-24 09:51:23 -04:00
|
|
|
stopbits=serial.STOPBITS_ONE,
|
G
2025-09-23 11:07:54 -04:00
|
|
|
)
|
|
|
|
|
except serial.SerialException as e:
|
|
|
|
|
print(f"Error connecting to {port}: {e}")
|
|
|
|
|
return None
|
|
|
|
|
|
G
2026-04-24 09:51:23 -04:00
|
|
|
|
G
2025-09-23 11:07:54 -04:00
|
|
|
def send_command(ser, command):
|
G
2025-09-25 20:53:22 -04:00
|
|
|
if ser and ser.is_open:
|
G
2026-04-24 09:51:23 -04:00
|
|
|
ser.write(command.encode("utf-8"))
|
|
|
|
|
|
G
2025-09-23 11:07:54 -04:00
|
|
|
|
|
|
|
|
def receive_feedback(ser):
|
G
2025-09-25 20:53:22 -04:00
|
|
|
if ser and ser.is_open:
|
G
2025-09-23 11:07:54 -04:00
|
|
|
try:
|
|
|
|
|
ser.flush()
|
|
|
|
|
raw_response = ser.readlines()
|
|
|
|
|
if raw_response:
|
|
|
|
|
rep = ""
|
|
|
|
|
for x in raw_response:
|
|
|
|
|
rep += str(x) + " ,"
|
|
|
|
|
rep = rep[2:].split("\\r")
|
|
|
|
|
return rep[-2]
|
|
|
|
|
except serial.SerialTimeoutException:
|
|
|
|
|
return ""
|
|
|
|
|
return ""
|
|
|
|
|
|
G
2026-04-24 09:51:23 -04:00
|
|
|
|
G
2025-09-23 11:07:54 -04:00
|
|
|
def scm_conf(port, baudrate, rx_cmd, tx_cmd):
|
|
|
|
|
ser = connect_serial(port, baudrate)
|
|
|
|
|
if ser:
|
G
2026-04-24 09:51:23 -04:00
|
|
|
for cmd in [rx_cmd, tx_cmd]:
|
|
|
|
|
feedback, attempt = None, 0
|
G
2025-09-23 11:07:54 -04:00
|
|
|
while feedback != "OK" and attempt < 5:
|
|
|
|
|
send_command(ser, cmd + "\r")
|
|
|
|
|
feedback = receive_feedback(ser)
|
|
|
|
|
attempt += 1
|
G
2025-09-25 20:53:22 -04:00
|
|
|
ser.close()
|
G
2025-09-23 11:07:54 -04:00
|
|
|
return True
|
|
|
|
|
return False
|
|
|
|
|
|
G
2026-04-24 09:51:23 -04:00
|
|
|
|
G
2025-09-23 11:07:54 -04:00
|
|
|
def gain_update(usrp_tx, usrp_rx, scm_tx, scm_rx):
|
|
|
|
|
global usrp_tx_gain, usrp_rx_gain, scm_tx_gain, scm_rx_gain
|
|
|
|
|
scm_change = False
|
G
2026-04-24 09:51:23 -04:00
|
|
|
|
G
2025-09-23 11:07:54 -04:00
|
|
|
if usrp_tx != usrp_tx_gain:
|
|
|
|
|
usrp_tx_gain = usrp_tx
|
|
|
|
|
os.system(f"tmux send-keys -t ran 'tx_gain 0 {usrp_tx_gain} ' C-m")
|
|
|
|
|
if usrp_rx != usrp_rx_gain:
|
|
|
|
|
usrp_rx_gain = usrp_rx
|
|
|
|
|
os.system(f"tmux send-keys -t ran 'rx_gain 0 {usrp_rx_gain} ' C-m")
|
|
|
|
|
if scm_tx != scm_tx_gain:
|
|
|
|
|
scm_tx_gain = scm_tx
|
|
|
|
|
scm_change = True
|
|
|
|
|
if scm_rx != scm_rx_gain:
|
|
|
|
|
scm_rx_gain = scm_rx
|
|
|
|
|
scm_change = True
|
|
|
|
|
|
|
|
|
|
if scm_change:
|
G
2026-04-24 09:51:23 -04:00
|
|
|
t_cmd = f"HW:GAIN 0 TX 0 {scm_tx_gain}"
|
|
|
|
|
r_cmd = f"HW:GAIN 1 RX 0 {scm_rx_gain}"
|
G
2025-09-23 11:07:54 -04:00
|
|
|
scm_conf("/dev/ttyUSB0", 115200, r_cmd, t_cmd)
|
|
|
|
|
scm_conf("/dev/ttyUSB1", 115200, r_cmd, t_cmd)
|
G
2025-09-25 20:53:22 -04:00
|
|
|
with config_lock:
|
|
|
|
|
config["scm_tx_gain"] = scm_tx_gain
|
|
|
|
|
config["scm_rx_gain"] = scm_rx_gain
|
G
2025-09-23 11:07:54 -04:00
|
|
|
|
G
2025-09-25 20:53:22 -04:00
|
|
|
with config_lock:
|
|
|
|
|
config["usrp_tx_gain"] = usrp_tx_gain
|
|
|
|
|
config["usrp_rx_gain"] = usrp_rx_gain
|
G
2025-09-23 11:07:54 -04:00
|
|
|
|
G
2025-09-25 20:53:22 -04:00
|
|
|
return True
|
G
2025-09-23 11:07:54 -04:00
|
|
|
|
G
2026-04-24 09:51:23 -04:00
|
|
|
|
|
|
|
|
def parse_iq_payload(payload):
|
|
|
|
|
if len(payload) <= 2:
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
iq_bytes = payload[2:]
|
|
|
|
|
usable_len = (len(iq_bytes) // 8) * 8
|
|
|
|
|
if usable_len == 0:
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
return np.frombuffer(iq_bytes[:usable_len], dtype=np.complex64)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def compute_power_db(iq):
|
|
|
|
|
if iq is None or len(iq) == 0:
|
|
|
|
|
return None
|
|
|
|
|
power = np.mean(np.abs(iq) ** 2)
|
|
|
|
|
if power <= 0:
|
|
|
|
|
return -120.0
|
|
|
|
|
return 10 * np.log10(power + 1e-12)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def data_receiver_thread():
|
|
|
|
|
global latest_iq_data, udp_sock
|
|
|
|
|
|
|
|
|
|
with config_lock:
|
|
|
|
|
iq_port = config["iq_port"]
|
|
|
|
|
|
|
|
|
|
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
|
|
|
|
sock.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 4 * 1024 * 1024)
|
|
|
|
|
sock.bind(("0.0.0.0", iq_port))
|
|
|
|
|
sock.settimeout(0.1)
|
|
|
|
|
|
|
|
|
|
with udp_sock_lock:
|
|
|
|
|
udp_sock = sock
|
|
|
|
|
|
|
|
|
|
print(f"Listening for IQ samples on UDP port {iq_port}")
|
|
|
|
|
|
|
|
|
|
total_packets = 0
|
|
|
|
|
total_bytes = 0
|
|
|
|
|
last_stat_time = time.time()
|
G
2025-09-25 20:53:22 -04:00
|
|
|
|
|
|
|
|
while not stop_event.is_set():
|
G
2026-04-24 09:51:23 -04:00
|
|
|
try:
|
|
|
|
|
payload, addr = sock.recvfrom(65535)
|
|
|
|
|
total_packets += 1
|
|
|
|
|
total_bytes += len(payload)
|
|
|
|
|
|
|
|
|
|
iq = parse_iq_payload(payload)
|
|
|
|
|
if iq is None or len(iq) == 0:
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
with latest_data_lock:
|
|
|
|
|
latest_iq_data = iq
|
|
|
|
|
|
|
|
|
|
with iq_buffer_lock:
|
|
|
|
|
iq_buffer.extend(iq)
|
|
|
|
|
|
|
|
|
|
now = time.time()
|
|
|
|
|
dt = now - last_stat_time
|
|
|
|
|
if dt >= 1.0:
|
|
|
|
|
bandwidth_mbps = (total_bytes * 8) / dt / 1e6
|
|
|
|
|
with config_lock:
|
|
|
|
|
config["packets_received"] = total_packets
|
|
|
|
|
config["iq_bandwidth_mbps"] = bandwidth_mbps
|
|
|
|
|
total_bytes = 0
|
|
|
|
|
last_stat_time = now
|
|
|
|
|
|
|
|
|
|
except socket.timeout:
|
|
|
|
|
continue
|
|
|
|
|
except OSError:
|
|
|
|
|
break
|
|
|
|
|
except Exception as e:
|
|
|
|
|
print(f"UDP receive error: {e}")
|
|
|
|
|
traceback.print_exc()
|
|
|
|
|
time.sleep(0.1)
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
sock.close()
|
|
|
|
|
except Exception:
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
with udp_sock_lock:
|
|
|
|
|
udp_sock = None
|
|
|
|
|
|
|
|
|
|
print("Data receiver thread stopped")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def generate_spectrum_plot():
|
|
|
|
|
while not stop_event.is_set():
|
G
2025-09-25 20:53:22 -04:00
|
|
|
if pause_event.is_set():
|
|
|
|
|
time.sleep(0.1)
|
|
|
|
|
continue
|
G
2026-04-24 09:51:23 -04:00
|
|
|
|
G
2025-09-25 20:53:22 -04:00
|
|
|
with config_lock:
|
|
|
|
|
sample_rate = config["sample_rate"]
|
|
|
|
|
window_ms = config["window_ms"]
|
|
|
|
|
center_freq = config["center_freq"]
|
|
|
|
|
NFFT = config["NFFT"]
|
|
|
|
|
streaming = config["streaming"]
|
|
|
|
|
|
|
|
|
|
if not streaming:
|
|
|
|
|
time.sleep(0.1)
|
|
|
|
|
continue
|
|
|
|
|
|
G
2026-04-24 09:51:23 -04:00
|
|
|
with iq_buffer_lock:
|
|
|
|
|
current_iq = np.array(iq_buffer, dtype=np.complex64)
|
|
|
|
|
|
|
|
|
|
if len(current_iq) == 0:
|
|
|
|
|
fig, axes = plt.subplots(2, 1, figsize=(14, 7))
|
|
|
|
|
fig.patch.set_facecolor('#1a1a2e')
|
|
|
|
|
for ax in axes:
|
|
|
|
|
ax.set_facecolor('#1a1a2e')
|
|
|
|
|
ax.set_xticks([])
|
|
|
|
|
ax.set_yticks([])
|
|
|
|
|
for spine in ax.spines.values():
|
|
|
|
|
spine.set_visible(False)
|
|
|
|
|
axes[0].text(0.5, 0.5, "Waiting for IQ samples on UDP port 5588 ...",
|
|
|
|
|
ha="center", va="center", transform=axes[0].transAxes,
|
|
|
|
|
fontsize=18, color="#00d4ff")
|
|
|
|
|
_save_and_emit(fig)
|
|
|
|
|
time.sleep(PLOT_REFRESH_SEC)
|
|
|
|
|
continue
|
G
2025-09-23 11:07:54 -04:00
|
|
|
|
|
|
|
|
try:
|
G
2026-04-24 09:51:23 -04:00
|
|
|
window_samples = int(sample_rate * window_ms / 1000)
|
|
|
|
|
if len(current_iq) > window_samples:
|
|
|
|
|
current_iq = current_iq[-window_samples:]
|
|
|
|
|
elif len(current_iq) < window_samples:
|
|
|
|
|
current_iq = np.pad(current_iq, (window_samples - len(current_iq), 0), mode="constant")
|
|
|
|
|
|
|
|
|
|
total_duration_s = len(current_iq) / sample_rate
|
|
|
|
|
total_duration_ms = total_duration_s * 1000.0
|
|
|
|
|
freq_low = center_freq - sample_rate / 2.0
|
|
|
|
|
freq_high = center_freq + sample_rate / 2.0
|
|
|
|
|
power_db = compute_power_db(current_iq)
|
|
|
|
|
|
|
|
|
|
if len(current_iq) > MAX_TIME_PLOT_POINTS:
|
|
|
|
|
step = max(1, len(current_iq) // MAX_TIME_PLOT_POINTS)
|
|
|
|
|
plot_iq = current_iq[::step]
|
|
|
|
|
else:
|
|
|
|
|
plot_iq = current_iq
|
|
|
|
|
|
|
|
|
|
times_ms = np.linspace(0, total_duration_ms, len(plot_iq), endpoint=False)
|
|
|
|
|
|
|
|
|
|
fig, axes = plt.subplots(
|
|
|
|
|
2, 1,
|
|
|
|
|
figsize=(14, 7),
|
|
|
|
|
gridspec_kw={"height_ratios": [1, 1]},
|
|
|
|
|
)
|
|
|
|
|
fig.patch.set_facecolor('#1a1a2e')
|
|
|
|
|
fig.subplots_adjust(left=0.07, right=0.98, top=0.94, bottom=0.08, hspace=0.32)
|
|
|
|
|
|
|
|
|
|
ax_time = axes[0]
|
|
|
|
|
ax_spec = axes[1]
|
|
|
|
|
|
|
|
|
|
for ax in axes:
|
|
|
|
|
ax.set_facecolor('#0f0f23')
|
|
|
|
|
ax.tick_params(colors='#aaa', labelsize=8)
|
|
|
|
|
for spine in ax.spines.values():
|
|
|
|
|
spine.set_color('#333')
|
|
|
|
|
|
|
|
|
|
ax_time.plot(times_ms, np.real(plot_iq), label="I", color="#00d4ff", linewidth=0.4, alpha=0.9)
|
|
|
|
|
ax_time.plot(times_ms, np.imag(plot_iq), label="Q", color="#ff6b6b", linewidth=0.4, alpha=0.9)
|
|
|
|
|
|
|
|
|
|
ax_time.set_xlim(0, total_duration_ms)
|
|
|
|
|
|
|
|
|
|
real_part = np.real(plot_iq)
|
|
|
|
|
imag_part = np.imag(plot_iq)
|
|
|
|
|
y_min = min(np.min(real_part), np.min(imag_part))
|
|
|
|
|
y_max = max(np.max(real_part), np.max(imag_part))
|
|
|
|
|
y_pad = max((y_max - y_min) * 0.03, 0.001)
|
|
|
|
|
ax_time.set_ylim(y_min - y_pad, y_max + y_pad)
|
|
|
|
|
ax_time.margins(x=0, y=0)
|
|
|
|
|
|
|
|
|
|
ax_time.set_xlabel("Time (ms)", color='#aaa', fontsize=9)
|
|
|
|
|
ax_time.set_ylabel("Amplitude", color='#aaa', fontsize=9)
|
|
|
|
|
ax_time.set_title(
|
|
|
|
|
f"IQ Time Series | Power: {power_db:.1f} dB | Samples: {len(current_iq):,}",
|
|
|
|
|
fontsize=10, fontweight="bold", color="#00d4ff", pad=8,
|
|
|
|
|
)
|
|
|
|
|
ax_time.grid(True, linestyle='--', linewidth=0.3, alpha=0.4, color='#444')
|
|
|
|
|
ax_time.legend(loc="upper right", fontsize=7, framealpha=0.6,
|
|
|
|
|
facecolor='#1a1a2e', edgecolor='#333', labelcolor='#ccc')
|
|
|
|
|
|
|
|
|
|
noverlap = min(NFFT - 1, int(NFFT * 0.5))
|
|
|
|
|
|
|
|
|
|
ax_spec.specgram(
|
|
|
|
|
current_iq,
|
G
2025-09-23 11:07:54 -04:00
|
|
|
Fs=sample_rate,
|
|
|
|
|
Fc=center_freq,
|
|
|
|
|
NFFT=NFFT,
|
G
2026-04-24 09:51:23 -04:00
|
|
|
noverlap=noverlap,
|
|
|
|
|
cmap="twilight",
|
|
|
|
|
mode="magnitude",
|
|
|
|
|
scale="dB",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
ax_spec.set_xlim(0, total_duration_s)
|
|
|
|
|
ax_spec.set_ylim(freq_low, freq_high)
|
|
|
|
|
ax_spec.margins(x=0, y=0)
|
|
|
|
|
|
|
|
|
|
ax_spec.xaxis.set_major_formatter(
|
|
|
|
|
ticker.FuncFormatter(lambda v, _: f"{v * 1e3:.1f}")
|
G
2025-09-23 11:07:54 -04:00
|
|
|
)
|
G
2026-04-24 09:51:23 -04:00
|
|
|
ax_spec.xaxis.set_minor_locator(ticker.AutoMinorLocator())
|
|
|
|
|
ax_spec.yaxis.set_major_formatter(
|
|
|
|
|
ticker.FuncFormatter(lambda v, _: f"{v / 1e9:.4f}")
|
G
2025-09-25 20:53:22 -04:00
|
|
|
)
|
G
2026-04-24 09:51:23 -04:00
|
|
|
|
|
|
|
|
ax_spec.set_xlabel("Time (ms)", color='#aaa', fontsize=9)
|
|
|
|
|
ax_spec.set_ylabel("Frequency (GHz)", color='#aaa', fontsize=9)
|
|
|
|
|
ax_spec.set_title("Spectrogram", fontsize=10, color="#aaa", pad=6)
|
|
|
|
|
ax_spec.grid(False)
|
|
|
|
|
|
|
|
|
|
_save_and_emit(fig)
|
|
|
|
|
|
G
2025-09-25 20:53:22 -04:00
|
|
|
except Exception as e:
|
|
|
|
|
print(f"Plot generation error: {e}")
|
G
2026-04-24 09:51:23 -04:00
|
|
|
traceback.print_exc()
|
|
|
|
|
|
|
|
|
|
time.sleep(PLOT_REFRESH_SEC)
|
|
|
|
|
|
G
2025-09-25 20:53:22 -04:00
|
|
|
print("Plotting thread stopped")
|
|
|
|
|
|
G
2026-04-24 09:51:23 -04:00
|
|
|
|
|
|
|
|
def _save_and_emit(fig):
|
|
|
|
|
buf = io.BytesIO()
|
|
|
|
|
fig.savefig(
|
|
|
|
|
buf,
|
|
|
|
|
format='png',
|
|
|
|
|
dpi=100,
|
|
|
|
|
facecolor=fig.get_facecolor(),
|
|
|
|
|
edgecolor='none',
|
|
|
|
|
pad_inches=0.05
|
|
|
|
|
)
|
|
|
|
|
buf.seek(0)
|
|
|
|
|
png_bytes = buf.read()
|
|
|
|
|
buf.close()
|
|
|
|
|
plt.close(fig)
|
|
|
|
|
|
|
|
|
|
with open(PLOT_PATH, "wb") as f:
|
|
|
|
|
f.write(png_bytes)
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
socketio.emit('plot_update', {'image': base64.b64encode(png_bytes).decode('utf-8')})
|
|
|
|
|
except Exception:
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
G
2025-09-25 20:53:22 -04:00
|
|
|
def start_plotting():
|
G
2026-04-24 09:51:23 -04:00
|
|
|
global plot_thread, rx_thread, latest_iq_data, iq_buffer
|
|
|
|
|
|
G
2025-09-25 20:53:22 -04:00
|
|
|
stop_event.clear()
|
|
|
|
|
pause_event.clear()
|
G
2026-04-24 09:51:23 -04:00
|
|
|
|
|
|
|
|
with latest_data_lock:
|
|
|
|
|
latest_iq_data = None
|
|
|
|
|
|
G
2025-09-25 20:53:22 -04:00
|
|
|
with config_lock:
|
|
|
|
|
config["streaming"] = True
|
G
2026-04-24 09:51:23 -04:00
|
|
|
config["packets_received"] = 0
|
|
|
|
|
config["iq_bandwidth_mbps"] = 0.0
|
|
|
|
|
max_samples = max(1, int(config["sample_rate"] * config["window_ms"] / 1000))
|
|
|
|
|
|
|
|
|
|
with iq_buffer_lock:
|
|
|
|
|
iq_buffer = deque(maxlen=max_samples)
|
|
|
|
|
|
|
|
|
|
if rx_thread is None or not rx_thread.is_alive():
|
|
|
|
|
rx_thread = threading.Thread(target=data_receiver_thread, daemon=True)
|
|
|
|
|
rx_thread.start()
|
|
|
|
|
print("UDP receiver thread started")
|
|
|
|
|
|
G
2025-09-25 20:53:22 -04:00
|
|
|
if plot_thread is None or not plot_thread.is_alive():
|
|
|
|
|
plot_thread = threading.Thread(target=generate_spectrum_plot, daemon=True)
|
|
|
|
|
plot_thread.start()
|
|
|
|
|
print("Plotting thread started")
|
G
2026-04-24 09:51:23 -04:00
|
|
|
|
G
2025-09-25 20:53:22 -04:00
|
|
|
return True
|
|
|
|
|
|
G
2026-04-24 09:51:23 -04:00
|
|
|
|
G
2025-09-25 20:53:22 -04:00
|
|
|
def stop_plotting():
|
G
2026-04-24 09:51:23 -04:00
|
|
|
global plot_thread, rx_thread, udp_sock
|
|
|
|
|
|
G
2025-09-25 20:53:22 -04:00
|
|
|
with config_lock:
|
|
|
|
|
config["streaming"] = False
|
G
2026-04-24 09:51:23 -04:00
|
|
|
|
G
2025-09-25 20:53:22 -04:00
|
|
|
stop_event.set()
|
G
2026-04-24 09:51:23 -04:00
|
|
|
|
|
|
|
|
with udp_sock_lock:
|
|
|
|
|
if udp_sock is not None:
|
|
|
|
|
try:
|
|
|
|
|
udp_sock.close()
|
|
|
|
|
except Exception:
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
if rx_thread and rx_thread.is_alive():
|
|
|
|
|
rx_thread.join(timeout=2.0)
|
|
|
|
|
|
G
2025-09-25 20:53:22 -04:00
|
|
|
if plot_thread and plot_thread.is_alive():
|
G
2026-04-24 09:51:23 -04:00
|
|
|
plot_thread.join(timeout=3.0)
|
|
|
|
|
|
|
|
|
|
fig, ax = plt.subplots(figsize=(14, 7))
|
|
|
|
|
fig.patch.set_facecolor('#1a1a2e')
|
|
|
|
|
ax.set_facecolor('#1a1a2e')
|
|
|
|
|
ax.text(0.5, 0.5, "Streaming Stopped\nClick Start to begin",
|
|
|
|
|
ha="center", va="center", transform=ax.transAxes,
|
|
|
|
|
fontsize=18, color="#00d4ff")
|
|
|
|
|
ax.set_xticks([])
|
|
|
|
|
ax.set_yticks([])
|
|
|
|
|
for spine in ax.spines.values():
|
|
|
|
|
spine.set_visible(False)
|
|
|
|
|
_save_and_emit(fig)
|
G
2025-09-25 20:53:22 -04:00
|
|
|
return True
|
G
2025-09-23 11:07:54 -04:00
|
|
|
|
G
2026-04-24 09:51:23 -04:00
|
|
|
|
G
2025-09-25 20:53:22 -04:00
|
|
|
def pause_plotting():
|
|
|
|
|
if pause_event.is_set():
|
|
|
|
|
pause_event.clear()
|
|
|
|
|
return "resumed"
|
|
|
|
|
else:
|
|
|
|
|
pause_event.set()
|
|
|
|
|
return "paused"
|
G
2025-09-23 11:07:54 -04:00
|
|
|
|
G
2026-04-24 09:51:23 -04:00
|
|
|
|
|
|
|
|
@app.route("/")
|
G
2025-09-23 11:07:54 -04:00
|
|
|
def index():
|
G
2026-04-24 09:51:23 -04:00
|
|
|
return render_template("index.html")
|
G
2025-09-23 11:07:54 -04:00
|
|
|
|
|
|
|
|
|
G
2026-04-24 09:51:23 -04:00
|
|
|
@app.route("/plot")
|
G
2025-09-23 11:07:54 -04:00
|
|
|
def plot():
|
G
2026-04-24 09:51:23 -04:00
|
|
|
return send_file(PLOT_PATH, mimetype="image/png")
|
G
2025-09-23 11:07:54 -04:00
|
|
|
|
G
2025-09-25 11:28:00 -04:00
|
|
|
|
G
2026-04-24 09:51:23 -04:00
|
|
|
@app.route("/start_stream", methods=["POST"])
|
G
2025-09-25 20:53:22 -04:00
|
|
|
def start_stream():
|
|
|
|
|
try:
|
G
2026-04-24 09:51:23 -04:00
|
|
|
start_plotting()
|
|
|
|
|
return jsonify(status="success", message="Streaming started")
|
G
2025-09-25 20:53:22 -04:00
|
|
|
except Exception as e:
|
G
2026-04-24 09:51:23 -04:00
|
|
|
return jsonify(status="error", message=str(e)), 500
|
G
2025-09-25 20:53:22 -04:00
|
|
|
|
G
2026-04-24 09:51:23 -04:00
|
|
|
|
|
|
|
|
@app.route("/stop_stream", methods=["POST"])
|
G
2025-09-25 20:53:22 -04:00
|
|
|
def stop_stream():
|
|
|
|
|
try:
|
G
2026-04-24 09:51:23 -04:00
|
|
|
stop_plotting()
|
|
|
|
|
return jsonify(status="success", message="Streaming stopped")
|
G
2025-09-25 20:53:22 -04:00
|
|
|
except Exception as e:
|
G
2026-04-24 09:51:23 -04:00
|
|
|
return jsonify(status="error", message=str(e)), 500
|
|
|
|
|
|
G
2025-09-25 20:53:22 -04:00
|
|
|
|
G
2026-04-24 09:51:23 -04:00
|
|
|
@app.route("/pause_stream", methods=["POST"])
|
G
2025-09-25 20:53:22 -04:00
|
|
|
def pause_stream():
|
|
|
|
|
try:
|
|
|
|
|
result = pause_plotting()
|
G
2026-04-24 09:51:23 -04:00
|
|
|
return jsonify(status="success", message=f"Streaming {result}", state=result)
|
G
2025-09-25 20:53:22 -04:00
|
|
|
except Exception as e:
|
G
2026-04-24 09:51:23 -04:00
|
|
|
return jsonify(status="error", message=str(e)), 500
|
G
2025-09-25 20:53:22 -04:00
|
|
|
|
G
2026-04-24 09:51:23 -04:00
|
|
|
|
|
|
|
|
@app.route("/get_stream_state")
|
G
2025-09-25 20:53:22 -04:00
|
|
|
def get_stream_state():
|
|
|
|
|
with config_lock:
|
|
|
|
|
streaming = config["streaming"]
|
|
|
|
|
paused = pause_event.is_set()
|
|
|
|
|
if streaming and not paused:
|
|
|
|
|
state = "running"
|
|
|
|
|
elif streaming and paused:
|
|
|
|
|
state = "paused"
|
G
2026-04-24 09:51:23 -04:00
|
|
|
else:
|
|
|
|
|
state = "stopped"
|
|
|
|
|
return jsonify(state=state)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.route("/update_params", methods=["POST"])
|
|
|
|
|
def update_params():
|
|
|
|
|
try:
|
|
|
|
|
with config_lock:
|
|
|
|
|
cf = request.form.get("center_freq", type=float)
|
|
|
|
|
sr = request.form.get("sample_rate", type=float)
|
|
|
|
|
nf = request.form.get("fft_size", type=int)
|
|
|
|
|
wm = request.form.get("window_ms", type=float)
|
|
|
|
|
if cf:
|
|
|
|
|
config["center_freq"] = cf
|
|
|
|
|
if sr:
|
|
|
|
|
config["sample_rate"] = sr
|
|
|
|
|
if nf:
|
|
|
|
|
config["NFFT"] = nf
|
|
|
|
|
if wm:
|
|
|
|
|
config["window_ms"] = wm
|
|
|
|
|
save_config()
|
|
|
|
|
return jsonify(status="success", message="Parameters updated")
|
|
|
|
|
except Exception as e:
|
|
|
|
|
return jsonify(status="error", message=str(e)), 500
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.route("/update_gains", methods=["POST"])
|
|
|
|
|
def update_gains():
|
|
|
|
|
try:
|
|
|
|
|
usrp_tx = request.form.get("usrp_tx_gain", type=float) or usrp_tx_gain
|
|
|
|
|
usrp_rx = request.form.get("usrp_rx_gain", type=float) or usrp_rx_gain
|
|
|
|
|
scm_tx = request.form.get("scm_tx_gain", type=float) or scm_tx_gain
|
|
|
|
|
scm_rx = request.form.get("scm_rx_gain", type=float) or scm_rx_gain
|
|
|
|
|
ok = gain_update(usrp_tx, usrp_rx, scm_tx, scm_rx)
|
|
|
|
|
if ok:
|
|
|
|
|
return jsonify(status="success", message="Gains updated")
|
|
|
|
|
return jsonify(status="error", message="Failed"), 500
|
|
|
|
|
except Exception as e:
|
|
|
|
|
return jsonify(status="error", message=str(e)), 500
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.route("/get_gains")
|
|
|
|
|
def get_gains():
|
|
|
|
|
return jsonify(
|
|
|
|
|
usrp_tx_gain=usrp_tx_gain,
|
|
|
|
|
usrp_rx_gain=usrp_rx_gain,
|
|
|
|
|
scm_tx_gain=scm_tx_gain,
|
|
|
|
|
scm_rx_gain=scm_rx_gain,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.route("/get_stats")
|
|
|
|
|
def get_stats():
|
|
|
|
|
with config_lock:
|
|
|
|
|
packets_received = config.get("packets_received", 0)
|
|
|
|
|
iq_bandwidth_mbps = config.get("iq_bandwidth_mbps", 0.0)
|
|
|
|
|
|
|
|
|
|
with iq_buffer_lock:
|
|
|
|
|
current_iq = np.array(iq_buffer, dtype=np.complex64)
|
|
|
|
|
|
|
|
|
|
power_db = compute_power_db(current_iq) if len(current_iq) > 0 else None
|
|
|
|
|
|
|
|
|
|
return jsonify(
|
|
|
|
|
current_slot="--",
|
|
|
|
|
packets_received=packets_received,
|
|
|
|
|
slots_correlated=0,
|
|
|
|
|
correlation_rate=0,
|
|
|
|
|
slot_rate=0,
|
|
|
|
|
iq_bandwidth_mbps=iq_bandwidth_mbps,
|
|
|
|
|
metadata_received=0,
|
|
|
|
|
slots_without_metadata=0,
|
|
|
|
|
avg_packets_per_slot=0,
|
|
|
|
|
power_db=round(power_db, 1) if power_db is not None else None,
|
|
|
|
|
allocated_rbs=0,
|
|
|
|
|
direction="--",
|
|
|
|
|
)
|
|
|
|
|
|
G
2025-09-25 20:53:22 -04:00
|
|
|
|
G
2025-09-25 11:28:00 -04:00
|
|
|
def save_config():
|
G
2025-09-25 20:53:22 -04:00
|
|
|
with config_lock:
|
|
|
|
|
cfg = dict(config)
|
|
|
|
|
try:
|
G
2026-04-24 09:51:23 -04:00
|
|
|
with open(os.path.join(os.getcwd(), "gain_viz.json"), "w") as f:
|
|
|
|
|
json.dump(cfg, f, indent=2, default=str)
|
G
2025-09-25 20:53:22 -04:00
|
|
|
except Exception as e:
|
|
|
|
|
print(f"Error saving config: {e}")
|
G
2025-09-25 11:28:00 -04:00
|
|
|
|
G
2026-04-24 09:51:23 -04:00
|
|
|
|
G
2025-09-23 15:30:12 -04:00
|
|
|
def main():
|
G
2025-09-23 11:07:54 -04:00
|
|
|
if not os.path.exists(PLOT_PATH):
|
G
2026-04-24 09:51:23 -04:00
|
|
|
fig, ax = plt.subplots(figsize=(14, 7))
|
|
|
|
|
fig.patch.set_facecolor('#1a1a2e')
|
|
|
|
|
ax.set_facecolor('#1a1a2e')
|
|
|
|
|
ax.text(0.5, 0.5, "Click Start to begin streaming",
|
|
|
|
|
ha="center", va="center", fontsize=18, color="#00d4ff")
|
|
|
|
|
ax.set_xticks([])
|
|
|
|
|
ax.set_yticks([])
|
|
|
|
|
for spine in ax.spines.values():
|
|
|
|
|
spine.set_visible(False)
|
|
|
|
|
fig.savefig(PLOT_PATH, facecolor=fig.get_facecolor())
|
G
2025-09-25 20:53:22 -04:00
|
|
|
plt.close(fig)
|
G
2025-09-23 11:07:54 -04:00
|
|
|
|
G
2026-04-24 09:51:23 -04:00
|
|
|
print("=" * 60)
|
|
|
|
|
print(" IQ Spectrum Analyzer (UDP IQ only, optimized)")
|
|
|
|
|
print("=" * 60)
|
|
|
|
|
|
|
|
|
|
socketio.run(
|
|
|
|
|
app,
|
|
|
|
|
host="0.0.0.0",
|
|
|
|
|
port=5000,
|
|
|
|
|
debug=True,
|
|
|
|
|
use_reloader=False,
|
|
|
|
|
allow_unsafe_werkzeug=True
|
|
|
|
|
)
|
|
|
|
|
|
G
2025-09-23 11:07:54 -04:00
|
|
|
|
G
2026-04-24 09:51:23 -04:00
|
|
|
if __name__ == "__main__":
|
G
2025-09-25 20:53:22 -04:00
|
|
|
main()
|