gain-viz/gain_viz/app.py

634 lines
18 KiB
Python
Raw Normal View History

G
2025-09-23 11:07:54 -04:00
from flask import Flask, render_template, send_file, request, jsonify
from flask_socketio import SocketIO
G
2025-09-23 11:07:54 -04:00
import numpy as np
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
import base64
import io
import traceback
import socket
from collections import deque
G
2025-09-23 11:07:54 -04:00
app = Flask(__name__)
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,
"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
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
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:
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,
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
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:
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
2025-09-23 11:07:54 -04:00
def scm_conf(port, baudrate, rx_cmd, tx_cmd):
ser = connect_serial(port, baudrate)
if ser:
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
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
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:
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
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():
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
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
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:
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,
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
)
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
)
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}")
traceback.print_exc()
time.sleep(PLOT_REFRESH_SEC)
G
2025-09-25 20:53:22 -04:00
print("Plotting thread stopped")
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():
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()
with latest_data_lock:
latest_iq_data = None
G
2025-09-25 20:53:22 -04:00
with config_lock:
config["streaming"] = True
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
2025-09-25 20:53:22 -04:00
return True
G
2025-09-25 20:53:22 -04:00
def stop_plotting():
global plot_thread, rx_thread, udp_sock
G
2025-09-25 20:53:22 -04:00
with config_lock:
config["streaming"] = False
G
2025-09-25 20:53:22 -04:00
stop_event.set()
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():
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
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
@app.route("/")
G
2025-09-23 11:07:54 -04:00
def index():
return render_template("index.html")
G
2025-09-23 11:07:54 -04:00
@app.route("/plot")
G
2025-09-23 11:07:54 -04:00
def plot():
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
@app.route("/start_stream", methods=["POST"])
G
2025-09-25 20:53:22 -04:00
def start_stream():
try:
start_plotting()
return jsonify(status="success", message="Streaming started")
G
2025-09-25 20:53:22 -04:00
except Exception as e:
return jsonify(status="error", message=str(e)), 500
G
2025-09-25 20:53:22 -04:00
@app.route("/stop_stream", methods=["POST"])
G
2025-09-25 20:53:22 -04:00
def stop_stream():
try:
stop_plotting()
return jsonify(status="success", message="Streaming stopped")
G
2025-09-25 20:53:22 -04:00
except Exception as e:
return jsonify(status="error", message=str(e)), 500
G
2025-09-25 20:53:22 -04:00
@app.route("/pause_stream", methods=["POST"])
G
2025-09-25 20:53:22 -04:00
def pause_stream():
try:
result = pause_plotting()
return jsonify(status="success", message=f"Streaming {result}", state=result)
G
2025-09-25 20:53:22 -04:00
except Exception as e:
return jsonify(status="error", message=str(e)), 500
G
2025-09-25 20:53:22 -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"
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:
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
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):
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
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
if __name__ == "__main__":
G
2025-09-25 20:53:22 -04:00
main()