Compare commits

..

No commits in common. "iq_meta" and "main" have entirely different histories.

19 changed files with 989 additions and 3586 deletions

Binary file not shown.

Binary file not shown.

View File

@ -1,73 +0,0 @@
Metadata-Version: 2.4
Name: gain_viz
Version: 0.1.0
Summary: Interactive srsRAN_Project gnb gain control and spectrum visualization tool
Author-email: "Qoherent Inc." <info@qoherent.ai>
Maintainer-email: Gael Kamga <gael@qoherent.ai>, Ashkan Beigi <ash@qoherent.ai>
Keywords: radio,rf,sdr,software-defined radio,5G,gnb,gNodeB,srsRAN_Project,SCM,SignalCraft Conditioning Mpdule,USRP
Requires-Python: >=3.8
Description-Content-Type: text/markdown
Requires-Dist: flask
Requires-Dist: matplotlib
Requires-Dist: numpy
Requires-Dist: pyzmq
Requires-Dist: pyserial
Requires-Dist: flask_socketio
# gain_viz
![Python](https://img.shields.io/badge/python-3.8%2B-blue)
![Flask](https://img.shields.io/badge/flask-2.x-orange)
# gain_viz
**gain_viz** is a Python-based web application for adjusting RF gain settings and visualizing their effect in real-time. It integrates with USRP and SCM devices, providing live IQ time-series and spectrum visualization.
---
## Features
- Adjust **USRP Tx/Rx gains** and **SCM Tx/Rx gains** from a web interface.
- Live IQ **time-series plot** in milliseconds.
- Live **spectrum visualization** (waterfall / spectrogram).
- Fast refresh for near real-time feedback.
- Responsive and clean web interface built with HTML/CSS/JS.
## Installation
Make sure you have **Python 3.8+** installed.
1. Clone the repository:
```bash
git clone https://riahub.ai/gael/gain-viz.git
cd gain-viz
```
2. Build and install
```bash
pip install --upgrade build
python3 -m build
pip install dist/gain_viz-0.1.0-py3-none-any.whl
export PATH=$PATH:~/.local/bin
source ~/.bashrc
```
## Usage
Run the application
```bash
gain_viz
```
Open your browser at http://localhost:5000
- Toggle the gain switches to enable input fields.
- Enter new gain values and press Update Gains.
- Observe the effect on the time-domain IQ plot and spectrum.

View File

@ -1,14 +0,0 @@
README.md
pyproject.toml
gain_viz/__init__.py
gain_viz/app.py
gain_viz/iq_metadata_interface.py
gain_viz.egg-info/PKG-INFO
gain_viz.egg-info/SOURCES.txt
gain_viz.egg-info/dependency_links.txt
gain_viz.egg-info/entry_points.txt
gain_viz.egg-info/requires.txt
gain_viz.egg-info/top_level.txt
gain_viz/static/plot.png
gain_viz/templates/ind.html
gain_viz/templates/index.html

View File

@ -1 +0,0 @@

View File

@ -1,2 +0,0 @@
[console_scripts]
gain_viz = gain_viz.app:main

View File

@ -1,6 +0,0 @@
flask
matplotlib
numpy
pyzmq
pyserial
flask_socketio

View File

@ -1 +0,0 @@
gain_viz

View File

@ -1,12 +0,0 @@
{
"usrp_tx_gain": 60.0,
"usrp_rx_gain": 20.0,
"scm_tx_gain": 30.0,
"scm_rx_gain": 20.0,
"sample_rate": 23040000.0,
"window_ms": 20.0,
"center_freq": 3430000000.0,
"NFFT": 1024,
"tcp_port": 5556,
"streaming": true
}

View File

@ -1,8 +1,6 @@
from flask import Flask, render_template, send_file, request, jsonify from flask import Flask, render_template, send_file, request, jsonify
from flask_socketio import SocketIO import zmq
import numpy as np import numpy as np
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt import matplotlib.pyplot as plt
import matplotlib.ticker as ticker import matplotlib.ticker as ticker
import os import os
@ -10,14 +8,8 @@ import threading
import time import time
import serial import serial
import json import json
import base64
import io
import traceback
import socket
from collections import deque
app = Flask(__name__) app = Flask(__name__)
socketio = SocketIO(app, cors_allowed_origins="*")
PLOT_PATH = os.path.join(os.getcwd(), "plot.png") PLOT_PATH = os.path.join(os.getcwd(), "plot.png")
# ----------------- Shared Config ----------------- # ----------------- Shared Config -----------------
@ -30,55 +22,42 @@ config = {
"window_ms": 20, "window_ms": 20,
"center_freq": 3.415e9, "center_freq": 3.415e9,
"NFFT": 1024, "NFFT": 1024,
"iq_port": 5588, "tcp_port": 5556,
"streaming": False, "streaming": False, # Added streaming state
"packets_received": 0,
"iq_bandwidth_mbps": 0.0,
} }
config_lock = threading.Lock() config_lock = threading.Lock()
# Global variables
usrp_tx_gain = config["usrp_tx_gain"] usrp_tx_gain = config["usrp_tx_gain"]
usrp_rx_gain = config["usrp_rx_gain"] usrp_rx_gain = config["usrp_rx_gain"]
scm_tx_gain = config["scm_tx_gain"] scm_tx_gain = config["scm_tx_gain"]
scm_rx_gain = config["scm_rx_gain"] scm_rx_gain = config["scm_rx_gain"]
# Plotting thread control
plot_thread = None plot_thread = None
rx_thread = None
stop_event = threading.Event() stop_event = threading.Event()
pause_event = threading.Event() pause_event = threading.Event()
latest_iq_data = None # ----------------- Serial / SCM -----------------
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
def connect_serial(port, baudrate=115200, timeout=1): def connect_serial(port, baudrate=115200, timeout=1):
"""Connect to a serial port with even parity."""
try: try:
return serial.Serial( ser = serial.Serial(
port=port, port=port,
baudrate=baudrate, baudrate=baudrate,
timeout=timeout, timeout=timeout,
bytesize=serial.EIGHTBITS, bytesize=serial.EIGHTBITS,
parity=serial.PARITY_EVEN, parity=serial.PARITY_EVEN,
stopbits=serial.STOPBITS_ONE, stopbits=serial.STOPBITS_ONE
) )
return ser
except serial.SerialException as e: except serial.SerialException as e:
print(f"Error connecting to {port}: {e}") print(f"Error connecting to {port}: {e}")
return None return None
def send_command(ser, command): def send_command(ser, command):
if ser and ser.is_open: if ser and ser.is_open:
ser.write(command.encode("utf-8")) ser.write(command.encode('utf-8'))
def receive_feedback(ser): def receive_feedback(ser):
if ser and ser.is_open: if ser and ser.is_open:
@ -95,12 +74,13 @@ def receive_feedback(ser):
return "" return ""
return "" return ""
def scm_conf(port, baudrate, rx_cmd, tx_cmd): def scm_conf(port, baudrate, rx_cmd, tx_cmd):
ser = connect_serial(port, baudrate) ser = connect_serial(port, baudrate)
commands = [rx_cmd, tx_cmd]
if ser: if ser:
for cmd in [rx_cmd, tx_cmd]: for cmd in commands:
feedback, attempt = None, 0 feedback = None
attempt = 0
while feedback != "OK" and attempt < 5: while feedback != "OK" and attempt < 5:
send_command(ser, cmd + "\r") send_command(ser, cmd + "\r")
feedback = receive_feedback(ser) feedback = receive_feedback(ser)
@ -109,29 +89,34 @@ def scm_conf(port, baudrate, rx_cmd, tx_cmd):
return True return True
return False return False
# ----------------- Gain Updates -----------------
def gain_update(usrp_tx, usrp_rx, scm_tx, scm_rx): def gain_update(usrp_tx, usrp_rx, scm_tx, scm_rx):
global usrp_tx_gain, usrp_rx_gain, scm_tx_gain, scm_rx_gain global usrp_tx_gain, usrp_rx_gain, scm_tx_gain, scm_rx_gain
scm_change = False
scm_change = False
if usrp_tx != usrp_tx_gain: if usrp_tx != usrp_tx_gain:
usrp_tx_gain = usrp_tx usrp_tx_gain = usrp_tx
os.system(f"tmux send-keys -t ran 'tx_gain 0 {usrp_tx_gain} ' C-m") os.system(f"tmux send-keys -t ran 'tx_gain 0 {usrp_tx_gain} ' C-m")
if usrp_rx != usrp_rx_gain: if usrp_rx != usrp_rx_gain:
usrp_rx_gain = usrp_rx usrp_rx_gain = usrp_rx
os.system(f"tmux send-keys -t ran 'rx_gain 0 {usrp_rx_gain} ' C-m") os.system(f"tmux send-keys -t ran 'rx_gain 0 {usrp_rx_gain} ' C-m")
if scm_tx != scm_tx_gain: if scm_tx != scm_tx_gain:
scm_tx_gain = scm_tx scm_tx_gain = scm_tx
scm_change = True scm_change = True
if scm_rx != scm_rx_gain: if scm_rx != scm_rx_gain:
scm_rx_gain = scm_rx scm_rx_gain = scm_rx
scm_change = True scm_change = True
t_cmd = f"HW:GAIN 0 TX 0 {scm_tx_gain}"
r_cmd = f"HW:GAIN 1 RX 0 {scm_rx_gain}"
if scm_change: 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}"
scm_conf("/dev/ttyUSB0", 115200, r_cmd, t_cmd) scm_conf("/dev/ttyUSB0", 115200, r_cmd, t_cmd)
scm_conf("/dev/ttyUSB1", 115200, r_cmd, t_cmd) scm_conf("/dev/ttyUSB1", 115200, r_cmd, t_cmd)
with config_lock: with config_lock:
config["scm_tx_gain"] = scm_tx_gain config["scm_tx_gain"] = scm_tx_gain
config["scm_rx_gain"] = scm_rx_gain config["scm_rx_gain"] = scm_rx_gain
@ -142,96 +127,14 @@ def gain_update(usrp_tx, usrp_rx, scm_tx, scm_rx):
return True return True
# ----------------- Plot Generation -----------------
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()
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(): def generate_spectrum_plot():
socket = None
iq_sample = np.zeros(1, dtype=np.complex64)
last_port = None
while not stop_event.is_set(): while not stop_event.is_set():
# Check if we're paused
if pause_event.is_set(): if pause_event.is_set():
time.sleep(0.1) time.sleep(0.1)
continue continue
@ -241,179 +144,118 @@ def generate_spectrum_plot():
window_ms = config["window_ms"] window_ms = config["window_ms"]
center_freq = config["center_freq"] center_freq = config["center_freq"]
NFFT = config["NFFT"] NFFT = config["NFFT"]
tcp_port = config["tcp_port"]
streaming = config["streaming"] streaming = config["streaming"]
# Only process if streaming is active
if not streaming: if not streaming:
time.sleep(0.1) time.sleep(0.1)
continue continue
with iq_buffer_lock: # Reconnect if port changed or socket is None
current_iq = np.array(iq_buffer, dtype=np.complex64) if socket is None or tcp_port != last_port:
if socket:
socket.close()
try:
context = zmq.Context()
socket = context.socket(zmq.SUB)
socket.setsockopt(zmq.CONFLATE, 1)
socket.setsockopt_string(zmq.SUBSCRIBE, "")
socket.setsockopt(zmq.RCVTIMEO, 1000)
socket.connect(f"tcp://localhost:{tcp_port}")
last_port = tcp_port
print(f"Connected to ZMQ on port {tcp_port}")
except Exception as e:
print(f"ZMQ connection error: {e}")
socket = None
time.sleep(1)
continue
if len(current_iq) == 0: window_samples = int(sample_rate * window_ms / 1000)
fig, axes = plt.subplots(2, 1, figsize=(14, 7)) if iq_sample.size != window_samples:
fig.patch.set_facecolor('#1a1a2e') iq_sample = np.zeros(window_samples, dtype=np.complex64)
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
try: try:
window_samples = int(sample_rate * window_ms / 1000) msg = socket.recv(zmq.NOBLOCK)
if len(current_iq) > window_samples: float_data = np.frombuffer(msg, dtype=np.float32)
current_iq = current_iq[-window_samples:] if float_data.size >= 2:
elif len(current_iq) < window_samples: complex_data = float_data.reshape(-1, 2)
current_iq = np.pad(current_iq, (window_samples - len(current_iq), 0), mode="constant") iq_all = complex_data[:, 0] + 1j * complex_data[:, 1]
if len(iq_all) >= window_samples:
iq_sample = iq_all[-window_samples:]
else:
iq_sample = np.pad(iq_all, (window_samples - len(iq_all), 0))
total_duration_s = len(current_iq) / sample_rate # Create plot
total_duration_ms = total_duration_s * 1000.0 fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(12, 6))
freq_low = center_freq - sample_rate / 2.0 fig.subplots_adjust(hspace=0.4)
freq_high = center_freq + sample_rate / 2.0
power_db = compute_power_db(current_iq)
if len(current_iq) > MAX_TIME_PLOT_POINTS: # Time-domain plot
step = max(1, len(current_iq) // MAX_TIME_PLOT_POINTS) times_ms = np.arange(len(iq_sample)) * 1000 / sample_rate
plot_iq = current_iq[::step] ax1.plot(times_ms, np.real(iq_sample), label="Real", color='b')
else: ax1.plot(times_ms, np.imag(iq_sample), label="Imag", color='r')
plot_iq = current_iq ax1.set_xlim(0, window_ms)
ax1.set_xlabel("Time (ms)")
ax1.set_ylabel("IQ Amplitude")
ax1.grid(True, which='both', linestyle='--', linewidth=0.5)
ax1.legend()
times_ms = np.linspace(0, total_duration_ms, len(plot_iq), endpoint=False) # Spectrogram
cmap = plt.get_cmap('twilight')
fig, axes = plt.subplots( ax2.specgram(
2, 1, iq_sample,
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,
Fs=sample_rate, Fs=sample_rate,
Fc=center_freq, Fc=center_freq,
NFFT=NFFT, NFFT=NFFT,
noverlap=noverlap, noverlap=512,
cmap="twilight", cmap=cmap
mode="magnitude",
scale="dB",
) )
ax2.set_xlabel("Time (ms)")
ax_spec.set_xlim(0, total_duration_s) ax2.set_ylabel("Frequency (Hz)")
ax_spec.set_ylim(freq_low, freq_high) ax2.grid(False)
ax_spec.margins(x=0, y=0) ax2.set_ylim(center_freq - sample_rate / 2,
center_freq + sample_rate / 2)
ax_spec.xaxis.set_major_formatter( ax2.xaxis.set_major_formatter(
ticker.FuncFormatter(lambda v, _: f"{v * 1e3:.1f}") ticker.FuncFormatter(lambda t, pos: '{0:g}'.format(t*1e3))
)
ax_spec.xaxis.set_minor_locator(ticker.AutoMinorLocator())
ax_spec.yaxis.set_major_formatter(
ticker.FuncFormatter(lambda v, _: f"{v / 1e9:.4f}")
) )
ax2.xaxis.set_minor_locator(ticker.AutoMinorLocator())
ax_spec.set_xlabel("Time (ms)", color='#aaa', fontsize=9) plt.savefig(PLOT_PATH, bbox_inches='tight')
ax_spec.set_ylabel("Frequency (GHz)", color='#aaa', fontsize=9) plt.close(fig)
ax_spec.set_title("Spectrogram", fontsize=10, color="#aaa", pad=6)
ax_spec.grid(False)
_save_and_emit(fig)
except zmq.Again:
# No new data
fig, ax = plt.subplots(figsize=(12, 6))
ax.text(0.5, 0.5, "Waiting for data...",
ha='center', va='center', transform=ax.transAxes, fontsize=16)
ax.set_title("Spectrum Analyzer - No Data (Streaming Active)")
plt.savefig(PLOT_PATH, bbox_inches='tight')
plt.close(fig)
except Exception as e: except Exception as e:
print(f"Plot generation error: {e}") print(f"Plot generation error: {e}")
traceback.print_exc() fig, ax = plt.subplots(figsize=(12, 6))
ax.text(0.5, 0.5, f"Error: {str(e)}",
ha='center', va='center', transform=ax.transAxes, fontsize=12)
ax.set_title("Spectrum Analyzer - Error")
plt.savefig(PLOT_PATH, bbox_inches='tight')
plt.close(fig)
time.sleep(PLOT_REFRESH_SEC) time.sleep(0.1)
# Cleanup when stopping
if socket:
socket.close()
print("Plotting thread stopped") 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
def start_plotting(): def start_plotting():
global plot_thread, rx_thread, latest_iq_data, iq_buffer """Start the plotting thread"""
global plot_thread, stop_event, pause_event
stop_event.clear() stop_event.clear()
pause_event.clear() pause_event.clear()
with latest_data_lock:
latest_iq_data = None
with config_lock: with config_lock:
config["streaming"] = True 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")
if plot_thread is None or not plot_thread.is_alive(): if plot_thread is None or not plot_thread.is_alive():
plot_thread = threading.Thread(target=generate_spectrum_plot, daemon=True) plot_thread = threading.Thread(target=generate_spectrum_plot, daemon=True)
@ -422,213 +264,194 @@ def start_plotting():
return True return True
def stop_plotting(): def stop_plotting():
global plot_thread, rx_thread, udp_sock """Stop the plotting thread"""
global plot_thread, stop_event
with config_lock: with config_lock:
config["streaming"] = False config["streaming"] = False
stop_event.set() 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)
if plot_thread and plot_thread.is_alive(): if plot_thread and plot_thread.is_alive():
plot_thread.join(timeout=3.0) plot_thread.join(timeout=2.0)
fig, ax = plt.subplots(figsize=(14, 7)) # Create stopped message plot
fig.patch.set_facecolor('#1a1a2e') fig, ax = plt.subplots(figsize=(12, 6))
ax.set_facecolor('#1a1a2e')
ax.text(0.5, 0.5, "Streaming Stopped\nClick Start to begin", ax.text(0.5, 0.5, "Streaming Stopped\nClick Start to begin",
ha="center", va="center", transform=ax.transAxes, ha='center', va='center', transform=ax.transAxes, fontsize=16)
fontsize=18, color="#00d4ff") ax.set_title("Spectrum Analyzer - Stopped")
ax.set_xticks([]) plt.savefig(PLOT_PATH, bbox_inches='tight')
ax.set_yticks([]) plt.close(fig)
for spine in ax.spines.values():
spine.set_visible(False) print("Plotting thread stopped")
_save_and_emit(fig)
return True return True
def pause_plotting(): def pause_plotting():
"""Pause the plotting updates"""
global pause_event
if pause_event.is_set(): if pause_event.is_set():
pause_event.clear() pause_event.clear()
print("Plotting resumed")
return "resumed" return "resumed"
else: else:
pause_event.set() pause_event.set()
print("Plotting paused")
return "paused" return "paused"
# ----------------- Flask Routes -----------------
@app.route("/") @app.route('/')
def index(): def index():
return render_template("index.html") return render_template('index.html')
@app.route('/update_gains', methods=['POST'])
def update_gains():
global usrp_tx_gain, usrp_rx_gain, scm_tx_gain, scm_rx_gain
@app.route("/plot") try:
usrp_tx = request.form.get('usrp_tx_gain', type=float)
usrp_rx = request.form.get('usrp_rx_gain', type=float)
scm_tx = request.form.get('scm_tx_gain', type=float)
scm_rx = request.form.get('scm_rx_gain', type=float)
if usrp_tx is None:
usrp_tx = usrp_tx_gain
if usrp_rx is None:
usrp_rx = usrp_rx_gain
if scm_tx is None:
scm_tx = scm_tx_gain
if scm_rx is None:
scm_rx = scm_rx_gain
success = gain_update(usrp_tx, usrp_rx, scm_tx, scm_rx)
if success:
return jsonify({"status": "success", "message": "Gains updated successfully"})
else:
return jsonify({"status": "error", "message": "Failed to update gains"}), 500
except Exception as e:
return jsonify({"status": "error", "message": f"Error: {str(e)}"}), 500
@app.route('/plot')
def plot(): def plot():
return send_file(PLOT_PATH, mimetype="image/png") try:
return send_file(PLOT_PATH, mimetype='image/png')
except Exception as e:
return send_file(PLOT_PATH, mimetype='image/png')
@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("/start_stream", methods=["POST"]) @app.route('/update_params', methods=['POST'])
def update_params():
try:
center_freq = request.form.get('center_freq', type=float)
sample_rate = request.form.get('sample_rate', type=float)
NFFT = request.form.get('fft_size', type=int)
window_ms = request.form.get('window_ms', type=float)
tcp_port = request.form.get('tcp_port', type=int)
if not all([center_freq, sample_rate, NFFT, window_ms, tcp_port]):
return jsonify({
'status': 'error',
'message': 'All parameters are required'
}), 400
with config_lock:
config["center_freq"] = center_freq
config["sample_rate"] = sample_rate
config["NFFT"] = NFFT
config["window_ms"] = window_ms
config["tcp_port"] = tcp_port
print(f"Updated params: center_freq={center_freq}, sample_rate={sample_rate}, NFFT={NFFT}, window_ms={window_ms}, tcp_port={tcp_port}")
save_config()
return jsonify({
'status': 'success',
'message': 'Parameters updated successfully'
})
except Exception as e:
print(f"Error updating params: {e}")
return jsonify({
'status': 'error',
'message': str(e)
}), 500
@app.route('/start_stream', methods=['POST'])
def start_stream(): def start_stream():
try: try:
start_plotting() success = start_plotting()
return jsonify(status="success", message="Streaming started") if success:
return jsonify({"status": "success", "message": "Streaming started"})
else:
return jsonify({"status": "error", "message": "Failed to start streaming"}), 500
except Exception as e: except Exception as e:
return jsonify(status="error", message=str(e)), 500 return jsonify({"status": "error", "message": f"Error: {str(e)}"}), 500
@app.route('/stop_stream', methods=['POST'])
@app.route("/stop_stream", methods=["POST"])
def stop_stream(): def stop_stream():
try: try:
stop_plotting() success = stop_plotting()
return jsonify(status="success", message="Streaming stopped") if success:
return jsonify({"status": "success", "message": "Streaming stopped"})
else:
return jsonify({"status": "error", "message": "Failed to stop streaming"}), 500
except Exception as e: except Exception as e:
return jsonify(status="error", message=str(e)), 500 return jsonify({"status": "error", "message": f"Error: {str(e)}"}), 500
@app.route('/pause_stream', methods=['POST'])
@app.route("/pause_stream", methods=["POST"])
def pause_stream(): def pause_stream():
try: try:
result = pause_plotting() result = pause_plotting()
return jsonify(status="success", message=f"Streaming {result}", state=result) return jsonify({"status": "success", "message": f"Streaming {result}", "state": result})
except Exception as e: except Exception as e:
return jsonify(status="error", message=str(e)), 500 return jsonify({"status": "error", "message": f"Error: {str(e)}"}), 500
@app.route('/get_stream_state', methods=['GET'])
@app.route("/get_stream_state")
def get_stream_state(): def get_stream_state():
with config_lock: with config_lock:
streaming = config["streaming"] streaming = config["streaming"]
paused = pause_event.is_set() paused = pause_event.is_set()
state = "stopped"
if streaming and not paused: if streaming and not paused:
state = "running" state = "running"
elif streaming and paused: elif streaming and paused:
state = "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="--",
)
return jsonify({"state": state})
def save_config(): def save_config():
with config_lock: with config_lock:
cfg = dict(config) cfg = dict(config)
try: try:
with open(os.path.join(os.getcwd(), "gain_viz.json"), "w") as f: with open(os.path.join(os.getcwd(), "gain_viz.json"), 'w') as f:
json.dump(cfg, f, indent=2, default=str) json.dump(cfg, f, indent=2)
except Exception as e: except Exception as e:
print(f"Error saving config: {e}") print(f"Error saving config: {e}")
# ----------------- Main -----------------
def main(): def main():
# Ensure placeholder image exists
if not os.path.exists(PLOT_PATH): if not os.path.exists(PLOT_PATH):
fig, ax = plt.subplots(figsize=(14, 7)) fig, ax = plt.subplots(figsize=(12, 6))
fig.patch.set_facecolor('#1a1a2e') ax.text(0.5, 0.5, "Click Start to begin streaming", ha='center', va='center', fontsize=16)
ax.set_facecolor('#1a1a2e') ax.set_title("Gain-Viz Spectrum Analyzer - Ready")
ax.text(0.5, 0.5, "Click Start to begin streaming", plt.savefig(PLOT_PATH)
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())
plt.close(fig) plt.close(fig)
print("=" * 60) print("Gain-Viz server started. Use the web interface to control streaming.")
print(" IQ Spectrum Analyzer (UDP IQ only, optimized)") app.run(host="0.0.0.0", port=5000, debug=True, use_reloader=False)
print("=" * 60)
socketio.run( if __name__ == '__main__':
app,
host="0.0.0.0",
port=5000,
debug=True,
use_reloader=False,
allow_unsafe_werkzeug=True
)
if __name__ == "__main__":
main() main()

View File

@ -1,16 +0,0 @@
{
"usrp_tx_gain": 60,
"usrp_rx_gain": 30,
"scm_tx_gain": 30,
"scm_rx_gain": 30,
"sample_rate": 23040000.0,
"window_ms": 5.0,
"center_freq": 3415000000.0,
"NFFT": 512,
"iq_port": 5588,
"metadata_port": 5589,
"streaming": true,
"show_rb_overlay": true,
"num_rbs": 273,
"subcarriers_per_rb": 12
}

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 689 KiB

View File

@ -1,954 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Gain-Viz — Spectrum Viewer</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<style>
:root {
--primary: #2563eb;
--primary-dark: #1d4ed8;
--primary-light: #dbeafe;
--bg: #f8fafc;
--card: #ffffff;
--muted: #64748b;
--border: #e2e8f0;
--radius: 12px;
--shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
--success: #10b981;
--warning: #f59e0b;
--error: #ef4444;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: 'Inter', system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial;
background: var(--bg);
color: #0f172a;
line-height: 1.5;
padding: 20px;
min-height: 100vh;
}
.container {
max-width: 1400px;
margin: 0 auto;
background: var(--card);
border-radius: var(--radius);
box-shadow: var(--shadow-lg);
overflow: hidden;
display: flex;
flex-direction: column;
height: calc(100vh - 40px);
}
header {
padding: 24px 32px;
background: linear-gradient(135deg, var(--primary), var(--primary-dark));
color: #fff;
display: flex;
flex-direction: column;
gap: 8px;
flex-shrink: 0;
}
header h1 {
margin: 0;
font-size: 1.8rem;
font-weight: 700;
display: flex;
align-items: center;
gap: 10px;
}
header h1::before {
content: "📡";
font-size: 1.5rem;
}
header p {
margin: 0;
opacity: .9;
font-size: 1rem;
font-weight: 400;
}
.content-wrapper {
display: flex;
flex: 1;
overflow: hidden;
}
.controls-panel {
width: 450px;
flex-shrink: 0;
padding: 24px;
border-right: 1px solid var(--border);
display: flex;
flex-direction: column;
gap: 24px;
overflow-y: auto;
}
.visualization-panel {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
.card {
background: #fff;
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 20px;
box-shadow: var(--shadow);
}
.card h2 {
margin: 0 0 16px 0;
font-size: 1.2rem;
font-weight: 600;
color: #0f172a;
display: flex;
align-items: center;
gap: 8px;
}
.card h2::before {
content: "⚙️";
font-size: 1.1rem;
}
/* Gain grid */
.gain-grid {
display: grid;
grid-template-columns: 1fr;
gap: 12px;
}
.gain-item {
display: flex;
align-items: center;
gap: 12px;
padding: 14px;
border-radius: 8px;
background: #f8fafc;
border: 1px solid var(--border);
transition: all 0.2s ease;
}
.gain-item:hover {
background: #f1f5f9;
border-color: #cbd5e1;
}
.gain-item label {
font-weight: 500;
font-size: 0.95rem;
width: 100px;
flex-shrink: 0;
}
.gain-input {
flex: 1;
}
.gain-input input {
width: 100%;
padding: 10px 12px;
border-radius: 6px;
border: 1px solid var(--border);
font-size: 0.95rem;
transition: border-color 0.2s;
}
.gain-input input:focus {
outline: none;
border-color: var(--primary);
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1);
}
.gain-input input:disabled {
background-color: #f1f5f9;
color: #94a3b8;
}
/* Toggle switch */
.toggle-switch {
position: relative;
width: 50px;
height: 26px;
flex-shrink: 0;
}
.toggle-switch input {
opacity: 0;
width: 0;
height: 0;
}
.slider {
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
background: #d1d5db;
border-radius: 24px;
transition: 0.25s;
cursor: pointer;
}
.slider:before {
content: "";
position: absolute;
left: 4px;
top: 4px;
width: 18px;
height: 18px;
background: white;
border-radius: 50%;
transition: 0.25s;
}
.toggle-switch input:checked + .slider {
background: var(--primary);
}
.toggle-switch input:checked + .slider:before {
transform: translateX(24px);
}
.form-actions {
display: flex;
justify-content: flex-end;
gap: 12px;
margin-top: 16px;
}
.btn {
padding: 10px 18px;
border-radius: 8px;
border: 0;
cursor: pointer;
font-weight: 600;
font-size: 0.9rem;
transition: all 0.2s;
display: flex;
align-items: center;
gap: 6px;
}
.btn.primary {
background: var(--primary);
color: #fff;
}
.btn.primary:hover:not(:disabled) {
background: var(--primary-dark);
transform: translateY(-1px);
box-shadow: 0 4px 6px -1px rgba(37, 99, 235, 0.3);
}
.btn.secondary {
background: #f1f5f9;
color: #475569;
}
.btn.secondary:hover:not(:disabled) {
background: #e2e8f0;
}
.btn.success {
background: var(--success);
color: #fff;
}
.btn.warning {
background: var(--warning);
color: #fff;
}
.btn.error {
background: var(--error);
color: #fff;
}
.btn:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none;
box-shadow: none;
}
.status {
margin-top: 12px;
padding: 12px;
border-radius: 6px;
display: none;
font-size: 0.9rem;
font-weight: 500;
}
.status.success {
background: rgba(16, 185, 129, 0.08);
color: #047857;
border: 1px solid rgba(16, 185, 129, 0.12);
}
.status.error {
background: rgba(239, 68, 68, 0.06);
color: #7f1d1d;
border: 1px solid rgba(239, 68, 68, 0.12);
}
/* Parameter grid */
.param-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 12px;
}
.param-item {
display: flex;
flex-direction: column;
gap: 6px;
}
.param-item label {
font-weight: 500;
font-size: 0.9rem;
color: #475569;
}
.param-item input {
padding: 10px 12px;
border-radius: 6px;
border: 1px solid var(--border);
font-size: 0.95rem;
transition: border-color 0.2s;
}
.param-item input:focus {
outline: none;
border-color: var(--primary);
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1);
}
/* Stream controls */
.stream-controls {
display: flex;
gap: 10px;
margin-bottom: 16px;
}
.stream-status {
padding: 8px 16px;
border-radius: 20px;
font-size: 0.9rem;
font-weight: 600;
display: inline-flex;
align-items: center;
gap: 6px;
}
.status-stopped {
background: #fef2f2;
color: var(--error);
border: 1px solid #fecaca;
}
.status-running {
background: #f0fdf4;
color: var(--success);
border: 1px solid #bbf7d0;
}
.status-paused {
background: #fffbeb;
color: var(--warning);
border: 1px solid #fed7aa;
}
/* Plot area */
.plot-area {
flex: 1;
padding: 24px;
background: #f8fafc;
display: flex;
flex-direction: column;
overflow: hidden;
}
.plot-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
}
.plot-header h3 {
margin: 0;
font-size: 1.3rem;
font-weight: 600;
color: #0f172a;
display: flex;
align-items: center;
gap: 8px;
}
.plot-header h3::before {
content: "📊";
}
.plot-controls {
display: flex;
gap: 10px;
align-items: center;
}
.plot-card {
background: #fff;
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 16px;
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
box-shadow: var(--shadow);
}
#plotImage {
width: 100%;
height: 100%;
object-fit: contain;
border-radius: 8px;
display: block;
background: #f8fafc;
border: 1px solid var(--border);
}
footer {
padding: 16px 32px;
text-align: center;
color: var(--muted);
font-size: 0.9rem;
border-top: 1px solid var(--border);
flex-shrink: 0;
}
/* Responsive design */
@media (max-width: 1024px) {
.content-wrapper {
flex-direction: column;
}
.controls-panel {
width: 100%;
border-right: none;
border-bottom: 1px solid var(--border);
max-height: 40vh;
}
.param-grid {
grid-template-columns: 1fr;
}
}
@media (max-width: 768px) {
body {
padding: 10px;
}
.container {
height: calc(100vh - 20px);
}
header {
padding: 20px;
}
.controls-panel, .plot-area {
padding: 16px;
}
.gain-item {
flex-direction: column;
align-items: flex-start;
gap: 10px;
}
.gain-item label {
width: 100%;
}
.gain-input {
width: 100%;
}
.toggle-switch {
align-self: flex-end;
}
.form-actions {
flex-direction: column;
}
.stream-controls {
flex-direction: column;
}
}
/* Custom scrollbar */
.controls-panel::-webkit-scrollbar {
width: 6px;
}
.controls-panel::-webkit-scrollbar-track {
background: #f1f5f9;
border-radius: 3px;
}
.controls-panel::-webkit-scrollbar-thumb {
background: #cbd5e1;
border-radius: 3px;
}
.controls-panel::-webkit-scrollbar-thumb:hover {
background: #94a3b8;
}
</style>
</head>
<body>
<div class="container">
<header>
<h1>Gain-Viz</h1>
<p>Interactive gain control and real-time RF visualization</p>
</header>
<div class="content-wrapper">
<div class="controls-panel">
<!-- Stream Controls Card -->
<div class="card">
<h2>Stream Controls</h2>
<div class="stream-controls">
<button class="btn success" id="startBtn">
<span>▶️</span> Start
</button>
<button class="btn warning" id="pauseBtn" disabled>
<span>⏸️</span> Pause
</button>
<button class="btn error" id="stopBtn" disabled>
<span>⏹️</span> Stop
</button>
</div>
<div class="stream-status status-stopped" id="streamStatus">
<span></span> Stopped
</div>
</div>
<!-- Gain Settings Card -->
<div class="card">
<h2>Gain Settings</h2>
<form id="gainForm">
<div class="gain-grid">
<!-- USRP Tx -->
<div class="gain-item">
<label for="usrp_tx_gain">USRP Tx</label>
<div class="gain-input">
<input type="number" id="usrp_tx_gain" name="usrp_tx_gain" value="60" disabled aria-label="USRP Tx Gain">
</div>
<label class="toggle-switch" title="Enable USRP Tx">
<input type="checkbox" class="gain-toggle" data-input="usrp_tx_gain" id="usrp_tx_toggle">
<span class="slider"></span>
</label>
</div>
<!-- SCM Tx -->
<div class="gain-item">
<label for="scm_tx_gain">SCM Tx</label>
<div class="gain-input">
<input type="number" id="scm_tx_gain" name="scm_tx_gain" value="30" disabled aria-label="SCM Tx Gain">
</div>
<label class="toggle-switch" title="Enable SCM Tx">
<input type="checkbox" class="gain-toggle" data-input="scm_tx_gain" id="scm_tx_toggle">
<span class="slider"></span>
</label>
</div>
<!-- USRP Rx -->
<div class="gain-item">
<label for="usrp_rx_gain">USRP Rx</label>
<div class="gain-input">
<input type="number" id="usrp_rx_gain" name="usrp_rx_gain" value="30" disabled aria-label="USRP Rx Gain">
</div>
<label class="toggle-switch" title="Enable USRP Rx">
<input type="checkbox" class="gain-toggle" data-input="usrp_rx_gain" id="usrp_rx_toggle">
<span class="slider"></span>
</label>
</div>
<!-- SCM Rx -->
<div class="gain-item">
<label for="scm_rx_gain">SCM Rx</label>
<div class="gain-input">
<input type="number" id="scm_rx_gain" name="scm_rx_gain" value="30" disabled aria-label="SCM Rx Gain">
</div>
<label class="toggle-switch" title="Enable SCM Rx">
<input type="checkbox" class="gain-toggle" data-input="scm_rx_gain" id="scm_rx_toggle">
<span class="slider"></span>
</label>
</div>
</div>
<div class="form-actions">
<button type="submit" class="btn primary" id="gainUpdateBtn">
<span>🔄</span> Update Gains
</button>
<button type="button" class="btn secondary" id="gainRefreshBtn" title="Reload gains from device">
<span></span> Refresh
</button>
</div>
<div id="gainStatusMessage" class="status"></div>
</form>
</div>
<!-- Parameters Card -->
<div class="card">
<h2>Spectrogram Parameters</h2>
<form id="paramForm">
<div class="param-grid">
<div class="param-item">
<label for="center_freq">Center Frequency (Hz)</label>
<input type="number" id="center_freq" name="center_freq" value="3415000000" step="1000000">
</div>
<div class="param-item">
<label for="sample_rate">Sample Rate (Hz)</label>
<input type="number" id="sample_rate" name="sample_rate" value="23040000" step="1000000">
</div>
<div class="param-item">
<label for="fft_size">FFT Size</label>
<input type="number" id="fft_size" name="fft_size" value="1024" step="128">
</div>
<div class="param-item">
<label for="window_ms">Window Size (ms)</label>
<input type="number" id="window_ms" name="window_ms" value="20" step="1">
</div>
<div class="param-item">
<label for="tcp_port">ZMQ Port</label>
<input type="number" id="tcp_port" name="tcp_port" value="5556" step="1">
</div>
</div>
<div class="form-actions">
<button type="submit" class="btn primary" id="paramUpdateBtn">
<span></span> Update Parameters
</button>
</div>
<div id="paramStatusMessage" class="status"></div>
</form>
</div>
</div>
<!-- Visualization Panel -->
<div class="visualization-panel">
<div class="plot-area">
<div class="plot-header">
<h3>Spectrum Analysis</h3>
<div class="plot-controls">
<button class="btn secondary" id="refreshPlotBtn">
<span>🔄</span> Refresh
</button>
</div>
</div>
<div class="plot-card">
<img id="plotImage" src="/plot" alt="Spectrum Analysis plot">
</div>
</div>
</div>
</div>
<footer>
Gain-Viz • Real-time RF monitoring
</footer>
</div>
<script>
(function(){
const plotImg = document.getElementById('plotImage');
let isPlotPaused = false;
let isStreaming = false;
let refreshInterval;
// Helper to show status for a specific element
function showStatus(id, message, type = 'success') {
const el = document.getElementById(id);
if (!el) return;
el.textContent = message;
el.className = 'status ' + (type === 'success' ? 'success' : 'error');
el.style.display = 'block';
if (type === 'success') setTimeout(()=>{ el.style.display = 'none'; }, 3000);
}
// Update stream status display
function updateStreamStatus(state) {
const statusEl = document.getElementById('streamStatus');
const startBtn = document.getElementById('startBtn');
const pauseBtn = document.getElementById('pauseBtn');
const stopBtn = document.getElementById('stopBtn');
statusEl.className = 'stream-status';
switch(state) {
case 'running':
statusEl.classList.add('status-running');
statusEl.innerHTML = '<span></span> Streaming';
startBtn.disabled = true;
pauseBtn.disabled = false;
stopBtn.disabled = false;
pauseBtn.innerHTML = '<span>⏸️</span> Pause';
isStreaming = true;
isPlotPaused = false;
break;
case 'paused':
statusEl.classList.add('status-paused');
statusEl.innerHTML = '<span></span> Paused';
startBtn.disabled = true;
pauseBtn.disabled = false;
stopBtn.disabled = false;
pauseBtn.innerHTML = '<span>▶️</span> Resume';
isStreaming = true;
isPlotPaused = true;
break;
case 'stopped':
statusEl.classList.add('status-stopped');
statusEl.innerHTML = '<span></span> Stopped';
startBtn.disabled = false;
pauseBtn.disabled = true;
stopBtn.disabled = true;
isStreaming = false;
isPlotPaused = false;
break;
}
}
// Stream control functions
async function startStream() {
try {
const response = await fetch('/start_stream', { method: 'POST' });
const data = await response.json();
if (data.status === 'success') {
updateStreamStatus('running');
showStatus('gainStatusMessage', 'Streaming started', 'success');
} else {
showStatus('gainStatusMessage', data.message, 'error');
}
} catch (err) {
console.error('Start stream error:', err);
showStatus('gainStatusMessage', 'Error starting stream', 'error');
}
}
async function stopStream() {
try {
const response = await fetch('/stop_stream', { method: 'POST' });
const data = await response.json();
if (data.status === 'success') {
updateStreamStatus('stopped');
showStatus('gainStatusMessage', 'Streaming stopped', 'success');
} else {
showStatus('gainStatusMessage', data.message, 'error');
}
} catch (err) {
console.error('Stop stream error:', err);
showStatus('gainStatusMessage', 'Error stopping stream', 'error');
}
}
async function pauseStream() {
try {
const response = await fetch('/pause_stream', { method: 'POST' });
const data = await response.json();
if (data.status === 'success') {
updateStreamStatus(data.state);
showStatus('gainStatusMessage', `Streaming ${data.state}`, 'success');
} else {
showStatus('gainStatusMessage', data.message, 'error');
}
} catch (err) {
console.error('Pause stream error:', err);
showStatus('gainStatusMessage', 'Error pausing stream', 'error');
}
}
// Check stream state periodically
async function checkStreamState() {
try {
const response = await fetch('/get_stream_state');
const data = await response.json();
updateStreamStatus(data.state);
} catch (err) {
console.error('Error checking stream state:', err);
}
}
// ---------------- Gain Form ----------------
const gainForm = document.getElementById('gainForm');
const toggles = document.querySelectorAll('.gain-toggle');
const gainUpdateBtn = document.getElementById('gainUpdateBtn');
const gainRefreshBtn = document.getElementById('gainRefreshBtn');
// enable/disable inputs depending on toggle
toggles.forEach(t => {
const inputId = t.dataset.input;
const inputEl = document.getElementById(inputId);
t.addEventListener('change', () => {
if (inputEl) {
inputEl.disabled = !t.checked;
if (t.checked) inputEl.focus();
}
});
});
// load existing gain values from server and populate inputs
function loadGains(){
fetch('/get_gains')
.then(r => r.json())
.then(data => {
if (!data) return;
if (data.usrp_tx_gain !== undefined) document.getElementById('usrp_tx_gain').value = data.usrp_tx_gain;
if (data.usrp_rx_gain !== undefined) document.getElementById('usrp_rx_gain').value = data.usrp_rx_gain;
if (data.scm_tx_gain !== undefined) document.getElementById('scm_tx_gain').value = data.scm_tx_gain;
if (data.scm_rx_gain !== undefined) document.getElementById('scm_rx_gain').value = data.scm_rx_gain;
// ensure toggles are off and inputs disabled initially
toggles.forEach(t => {
t.checked = false;
const input = document.getElementById(t.dataset.input);
if (input) input.disabled = true;
});
})
.catch(err => console.error('loadGains error', err));
}
gainForm.addEventListener('submit', function(e){
e.preventDefault();
const formData = new FormData();
let has = false;
toggles.forEach(t => {
if (t.checked) {
const inputId = t.dataset.input;
const val = document.getElementById(inputId).value;
formData.append(inputId, val);
has = true;
}
});
if (!has) {
showStatus('gainStatusMessage', 'Please enable at least one gain to update', 'error');
return;
}
gainUpdateBtn.disabled = true;
fetch('/update_gains', { method:'POST', body: formData })
.then(r => r.json())
.then(data => {
gainUpdateBtn.disabled = false;
if (data && data.status === 'success') {
showStatus('gainStatusMessage', data.message || 'Gains updated', 'success');
// reset toggles
toggles.forEach(t => {
t.checked = false;
const input = document.getElementById(t.dataset.input);
if (input) input.disabled = true;
});
// reload gains from server to reflect current state
loadGains();
} else {
showStatus('gainStatusMessage', data.message || 'Error updating gains', 'error');
}
})
.catch(err => {
gainUpdateBtn.disabled = false;
console.error(err);
showStatus('gainStatusMessage', 'Server error', 'error');
});
});
gainRefreshBtn.addEventListener('click', function(){
loadGains();
showStatus('gainStatusMessage','Gains reloaded','success');
});
// ---------------- Params Form ----------------
const paramForm = document.getElementById('paramForm');
paramForm.addEventListener('submit', function(e){
e.preventDefault();
const btn = document.getElementById('paramUpdateBtn');
const formData = new FormData(paramForm);
btn.disabled = true;
fetch('/update_params', { method:'POST', body: formData })
.then(r => r.json())
.then(data => {
btn.disabled = false;
if (data && data.status === 'success') {
showStatus('paramStatusMessage', data.message || 'Parameters updated', 'success');
} else {
showStatus('paramStatusMessage', data.message || 'Error updating parameters', 'error');
}
})
.catch(err => {
btn.disabled = false;
console.error(err);
showStatus('paramStatusMessage','Server error','error');
});
});
// ---------------- Plot refresh ----------------
function refreshPlot(){
if (!isStreaming || isPlotPaused) return;
// use cache-buster to avoid cached/partial file
plotImg.src = '/plot?_ts=' + Date.now();
}
// Start auto refresh only when streaming
function startAutoRefresh() {
refreshInterval = setInterval(refreshPlot, 500);
}
// Manual refresh button
document.getElementById('refreshPlotBtn').addEventListener('click', function() {
refreshPlot();
});
// Stream control buttons
document.getElementById('startBtn').addEventListener('click', startStream);
document.getElementById('stopBtn').addEventListener('click', stopStream);
document.getElementById('pauseBtn').addEventListener('click', pauseStream);
// Check stream state every 2 seconds
setInterval(checkStreamState, 2000);
// initial load
loadGains();
checkStreamState();
startAutoRefresh();
})();
</script>
</body>
</html>

File diff suppressed because it is too large Load Diff

BIN
plot.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

View File

@ -36,8 +36,7 @@ dependencies = [
"matplotlib", "matplotlib",
"numpy", "numpy",
"pyzmq", "pyzmq",
"pyserial", "pyserial"
"flask_socketio"
] ]
[project.scripts] [project.scripts]

View File

@ -1,494 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>IQ Spectrum Analyzer</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.0.1/socket.io.js"></script>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: 'Segoe UI', Arial, sans-serif;
margin: 0; padding: 20px;
background: #1a1a2e; color: #eee;
min-height: 100vh;
}
.container {
max-width: 1400px;
margin: 0 auto;
}
h1 {
color: #00d4ff;
text-align: center;
margin-bottom: 20px;
font-size: 1.8em;
}
/* ---- Controls ---- */
.controls {
display: flex;
justify-content: center;
gap: 12px;
margin: 15px 0;
}
button {
padding: 10px 28px;
cursor: pointer;
background: #00d4ff;
color: #1a1a2e;
border: none;
border-radius: 8px;
font-weight: bold;
font-size: 14px;
transition: all 0.2s;
}
button:hover { background: #33e0ff; transform: translateY(-1px); }
button:active { transform: translateY(0); }
button.stop-btn { background: #ff6b6b; color: #fff; }
button.stop-btn:hover { background: #ff8a8a; }
button.pause-btn { background: #ffd93d; color: #1a1a2e; }
button.pause-btn:hover { background: #ffe066; }
.stream-status {
text-align: center;
padding: 6px 16px;
border-radius: 20px;
font-size: 13px;
font-weight: 600;
display: inline-block;
margin: 0 auto;
}
.status-running { background: rgba(0,212,255,0.15); color: #00d4ff; border: 1px solid rgba(0,212,255,0.3); }
.status-stopped { background: rgba(255,107,107,0.15); color: #ff6b6b; border: 1px solid rgba(255,107,107,0.3); }
.status-paused { background: rgba(255,217,61,0.15); color: #ffd93d; border: 1px solid rgba(255,217,61,0.3); }
/* ---- Stats bar ---- */
.stats-bar {
background: #16213e;
padding: 14px 20px;
border-radius: 10px;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
gap: 10px;
margin: 15px 0;
border: 1px solid #1e3050;
}
.stat-item { text-align: center; }
.stat-value {
color: #00d4ff;
font-size: 18px;
font-weight: bold;
font-variant-numeric: tabular-nums;
}
.stat-value.good { color: #00d4ff; }
.stat-value.warn { color: #ffd93d; }
.stat-value.bad { color: #ff6b6b; }
.stat-label {
font-size: 11px;
color: #8899aa;
margin-top: 2px;
}
/* ---- PLOT CONTAINER — this is the key fix ---- */
.plot-container {
background: #0f0f23;
padding: 0; /* NO padding so image fills fully */
border-radius: 10px;
margin: 15px 0;
border: 1px solid #1e3050;
overflow: hidden; /* clip any overflow */
line-height: 0; /* remove gap below inline img */
}
#plot {
width: 100%; /* fill container width */
height: auto; /* maintain aspect ratio */
display: block; /* remove inline spacing */
border-radius: 10px;
}
/* ---- Parameters panel ---- */
.params-panel {
background: #16213e;
padding: 20px;
border-radius: 10px;
margin: 15px 0;
border: 1px solid #1e3050;
}
.params-panel h3 {
color: #00d4ff;
margin-bottom: 15px;
font-size: 1.1em;
}
.param-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 12px;
}
.param-grid label {
font-size: 12px;
color: #8899aa;
display: block;
margin-bottom: 4px;
}
.param-grid input {
width: 100%;
padding: 8px 10px;
background: #0f0f23;
border: 1px solid #2a3a5e;
border-radius: 6px;
color: #eee;
font-size: 14px;
transition: border-color 0.2s;
}
.param-grid input:focus {
outline: none;
border-color: #00d4ff;
box-shadow: 0 0 0 2px rgba(0,212,255,0.15);
}
.apply-btn-wrapper {
text-align: center;
margin-top: 15px;
}
/* ---- Gain panel ---- */
.gain-panel {
background: #16213e;
padding: 20px;
border-radius: 10px;
margin: 15px 0;
border: 1px solid #1e3050;
}
.gain-panel h3 {
color: #00d4ff;
margin-bottom: 15px;
font-size: 1.1em;
}
.gain-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 12px;
}
.gain-grid label {
font-size: 12px;
color: #8899aa;
display: block;
margin-bottom: 4px;
}
.gain-grid input {
width: 100%;
padding: 8px 10px;
background: #0f0f23;
border: 1px solid #2a3a5e;
border-radius: 6px;
color: #eee;
font-size: 14px;
}
.gain-grid input:focus {
outline: none;
border-color: #00d4ff;
}
/* ---- Footer ---- */
.footer {
text-align: center;
color: #445;
font-size: 12px;
margin-top: 20px;
padding: 10px;
}
</style>
</head>
<body>
<div class="container">
<h1>📡 IQ Spectrum Analyzer</h1>
<!-- Stream controls -->
<div class="controls">
<button onclick="startStream()" id="startBtn">▶ Start</button>
<button class="pause-btn" onclick="pauseStream()" id="pauseBtn">⏸ Pause</button>
<button class="stop-btn" onclick="stopStream()" id="stopBtn">⏹ Stop</button>
</div>
<div style="text-align:center; margin-bottom:10px;">
<span class="stream-status status-stopped" id="streamStatus">● Stopped</span>
</div>
<!-- Stats bar -->
<div class="stats-bar">
<div class="stat-item">
<div class="stat-value" id="slot">--</div>
<div class="stat-label">Current Slot</div>
</div>
<div class="stat-item">
<div class="stat-value" id="direction">--</div>
<div class="stat-label">Direction</div>
</div>
<div class="stat-item">
<div class="stat-value" id="power">--</div>
<div class="stat-label">Power (dB)</div>
</div>
<div class="stat-item">
<div class="stat-value" id="rbs">0</div>
<div class="stat-label">Allocated RBs</div>
</div>
<div class="stat-item">
<div class="stat-value" id="packets">0</div>
<div class="stat-label">IQ Packets</div>
</div>
<div class="stat-item">
<div class="stat-value" id="slots">0</div>
<div class="stat-label">Slots Correlated</div>
</div>
<div class="stat-item">
<div class="stat-value" id="correlation">0%</div>
<div class="stat-label">Correlation</div>
</div>
<div class="stat-item">
<div class="stat-value" id="slotRate">0</div>
<div class="stat-label">Slots/s</div>
</div>
<div class="stat-item">
<div class="stat-value" id="bandwidth">0</div>
<div class="stat-label">IQ BW (Mbps)</div>
</div>
</div>
<!-- Plot — no padding, image fills container -->
<div class="plot-container">
<img id="plot" src="/plot" alt="IQ Spectrum Plot">
</div>
<!-- Parameters -->
<div class="params-panel">
<h3>⚙ Spectrogram Parameters</h3>
<div class="param-grid">
<div>
<label>Center Freq (Hz)</label>
<input type="number" id="center_freq" value="3415000000" step="1000000">
</div>
<div>
<label>Sample Rate (Hz)</label>
<input type="number" id="sample_rate" value="23040000" step="1000000">
</div>
<div>
<label>FFT Size</label>
<input type="number" id="fft_size" value="1024" step="128">
</div>
<div>
<label>Window (ms)</label>
<input type="number" id="window_ms" value="20" step="1">
</div>
</div>
<div class="apply-btn-wrapper">
<button onclick="updateParams()">⚡ Apply Parameters</button>
</div>
</div>
<!-- Gains -->
<div class="gain-panel">
<h3>🎚 Gain Settings</h3>
<div class="gain-grid">
<div>
<label>USRP Tx Gain</label>
<input type="number" id="usrp_tx_gain" value="60">
</div>
<div>
<label>USRP Rx Gain</label>
<input type="number" id="usrp_rx_gain" value="30">
</div>
<div>
<label>SCM Tx Gain</label>
<input type="number" id="scm_tx_gain" value="30">
</div>
<div>
<label>SCM Rx Gain</label>
<input type="number" id="scm_rx_gain" value="30">
</div>
</div>
<div class="apply-btn-wrapper">
<button onclick="updateGains()">🔄 Update Gains</button>
<button onclick="loadGains()" style="background:#2a3a5e; color:#eee; margin-left:8px;">↻ Refresh</button>
</div>
</div>
<div class="footer">
IQ Spectrum Analyzer • Real-time RF monitoring with IQmetadata correlation
</div>
</div>
<script>
(function() {
const socket = io();
const plotImg = document.getElementById('plot');
let isStreaming = false;
// ---- WebSocket: real-time plot push ----
socket.on('plot_update', function(data) {
if (data && data.image) {
plotImg.src = 'data:image/png;base64,' + data.image;
}
});
// ---- Fallback: poll /plot if websocket image stale ----
// (The websocket push is primary; this is backup)
setInterval(function() {
if (isStreaming && !document.hidden) {
// Only use polling as fallback — websocket handles most updates
// Uncomment below if websocket is unreliable:
// plotImg.src = '/plot?_ts=' + Date.now();
}
}, 2000);
// ---- Stream controls ----
window.startStream = function() {
fetch('/start_stream', {method: 'POST'})
.then(r => r.json())
.then(d => { if (d.status === 'success') updateStatus('running'); });
};
window.stopStream = function() {
fetch('/stop_stream', {method: 'POST'})
.then(r => r.json())
.then(d => { if (d.status === 'success') updateStatus('stopped'); });
};
window.pauseStream = function() {
fetch('/pause_stream', {method: 'POST'})
.then(r => r.json())
.then(d => { if (d.status === 'success') updateStatus(d.state); });
};
function updateStatus(state) {
const el = document.getElementById('streamStatus');
el.className = 'stream-status';
switch(state) {
case 'running':
el.className += ' status-running';
el.textContent = '● Streaming';
isStreaming = true;
break;
case 'paused':
el.className += ' status-paused';
el.textContent = '● Paused';
isStreaming = true;
break;
default:
el.className += ' status-stopped';
el.textContent = '● Stopped';
isStreaming = false;
}
}
// ---- Stats polling ----
setInterval(function() {
fetch('/get_stats')
.then(r => r.json())
.then(d => {
document.getElementById('slot').textContent = d.current_slot;
document.getElementById('direction').textContent = d.direction || '--';
document.getElementById('power').textContent =
d.power_db !== null ? d.power_db.toFixed(1) : '--';
document.getElementById('rbs').textContent = d.allocated_rbs || 0;
document.getElementById('packets').textContent =
d.packets_received.toLocaleString();
document.getElementById('slots').textContent =
d.slots_correlated.toLocaleString();
// Correlation rate with color coding
const corrEl = document.getElementById('correlation');
const corrVal = d.correlation_rate || 0;
corrEl.textContent = corrVal.toFixed(1) + '%';
corrEl.className = 'stat-value ' +
(corrVal > 90 ? 'good' : corrVal > 50 ? 'warn' : 'bad');
document.getElementById('slotRate').textContent =
(d.slot_rate || 0).toFixed(1);
document.getElementById('bandwidth').textContent =
(d.iq_bandwidth_mbps || 0).toFixed(1);
})
.catch(() => {});
}, 1000);
// ---- Stream state polling ----
setInterval(function() {
fetch('/get_stream_state')
.then(r => r.json())
.then(d => updateStatus(d.state))
.catch(() => {});
}, 3000);
// ---- Parameters ----
window.updateParams = function() {
const formData = new FormData();
['center_freq', 'sample_rate', 'fft_size', 'window_ms'].forEach(id => {
formData.append(id, document.getElementById(id).value);
});
fetch('/update_params', {method: 'POST', body: formData})
.then(r => r.json())
.then(d => {
if (d.status === 'success') {
showToast('Parameters updated');
}
});
};
// ---- Gains ----
window.updateGains = function() {
const formData = new FormData();
['usrp_tx_gain', 'usrp_rx_gain', 'scm_tx_gain', 'scm_rx_gain'].forEach(id => {
formData.append(id, document.getElementById(id).value);
});
fetch('/update_gains', {method: 'POST', body: formData})
.then(r => r.json())
.then(d => {
if (d.status === 'success') showToast('Gains updated');
});
};
window.loadGains = function() {
fetch('/get_gains')
.then(r => r.json())
.then(d => {
if (d.usrp_tx_gain !== undefined)
document.getElementById('usrp_tx_gain').value = d.usrp_tx_gain;
if (d.usrp_rx_gain !== undefined)
document.getElementById('usrp_rx_gain').value = d.usrp_rx_gain;
if (d.scm_tx_gain !== undefined)
document.getElementById('scm_tx_gain').value = d.scm_tx_gain;
if (d.scm_rx_gain !== undefined)
document.getElementById('scm_rx_gain').value = d.scm_rx_gain;
showToast('Gains refreshed');
});
};
// ---- Toast notification ----
function showToast(msg) {
let toast = document.createElement('div');
toast.textContent = msg;
toast.style.cssText =
'position:fixed; bottom:30px; left:50%; transform:translateX(-50%);' +
'background:#00d4ff; color:#1a1a2e; padding:10px 24px; border-radius:8px;' +
'font-weight:bold; font-size:14px; z-index:9999; opacity:0;' +
'transition:opacity 0.3s;';
document.body.appendChild(toast);
requestAnimationFrame(() => toast.style.opacity = '1');
setTimeout(() => {
toast.style.opacity = '0';
setTimeout(() => toast.remove(), 300);
}, 2000);
}
// ---- Init ----
loadGains();
})();
</script>
</body>
</html>