Compare commits

..

No commits in common. "tmux-integration" and "main" have entirely different histories.

16 changed files with 277 additions and 704 deletions

View File

@ -35,7 +35,7 @@ cd gain-viz
```bash ```bash
pip install --upgrade build pip install --upgrade build
python3 -m build python3 -m build
pip install dist/gain_viz-0.1.1-py3-none-any.whl pip install dist/gain_viz-0.1.0-py3-none-any.whl
export PATH=$PATH:~/.local/bin export PATH=$PATH:~/.local/bin
source ~/.bashrc source ~/.bashrc

Binary file not shown.

Binary file not shown.

View File

@ -1,72 +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
# 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,12 +0,0 @@
README.md
pyproject.toml
gain_viz/__init__.py
gain_viz/app.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/index.html

View File

@ -1 +0,0 @@

View File

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

View File

@ -1,5 +0,0 @@
flask
matplotlib
numpy
pyzmq
pyserial

View File

@ -1 +0,0 @@
gain_viz

View File

@ -1,12 +0,0 @@
{
"usrp_tx_gain": 20.0,
"usrp_rx_gain": 10.0,
"scm_tx_gain": 30,
"scm_rx_gain": 30,
"sample_rate": 46040000.0,
"window_ms": 10.0,
"center_freq": 3425000000.0,
"NFFT": 1024,
"tcp_port": 5556,
"streaming": true
}

View File

@ -8,18 +8,9 @@ import threading
import time import time
import serial import serial
import json import json
import subprocess
import io
import base64 # ADD THIS IMPORT
from PIL import Image
from flask_socketio import SocketIO, emit
# Define PLOT_PATH at the top level
PLOT_PATH = os.path.join(os.getcwd(), "plot.png")
app = Flask(__name__) app = Flask(__name__)
# Initialize SocketIO with proper configuration PLOT_PATH = os.path.join(os.getcwd(), "plot.png")
socketio = SocketIO(app, cors_allowed_origins="*", async_mode='threading')
# ----------------- Shared Config ----------------- # ----------------- Shared Config -----------------
config = { config = {
@ -32,7 +23,7 @@ config = {
"center_freq": 3.415e9, "center_freq": 3.415e9,
"NFFT": 1024, "NFFT": 1024,
"tcp_port": 5556, "tcp_port": 5556,
"streaming": False, "streaming": False, # Added streaming state
} }
config_lock = threading.Lock() config_lock = threading.Lock()
@ -47,19 +38,6 @@ plot_thread = None
stop_event = threading.Event() stop_event = threading.Event()
pause_event = threading.Event() pause_event = threading.Event()
# TMUX output capture
tmux_output = []
tmux_lock = threading.Lock()
tmux_thread = None
tmux_stop_event = threading.Event()
# In-memory plot storage
plot_lock = threading.Lock()
plot_buffer = io.BytesIO()
# WebSocket sender thread
websocket_thread = None
# ----------------- Serial / SCM ----------------- # ----------------- Serial / SCM -----------------
def connect_serial(port, baudrate=115200, timeout=1): def connect_serial(port, baudrate=115200, timeout=1):
"""Connect to a serial port with even parity.""" """Connect to a serial port with even parity."""
@ -111,60 +89,6 @@ def scm_conf(port, baudrate, rx_cmd, tx_cmd):
return True return True
return False return False
# ----------------- TMUX Output Capture -----------------
def capture_tmux_output():
"""Capture tmux output in a separate thread"""
while not tmux_stop_event.is_set():
try:
# First check if the tmux session exists
check_cmd = "tmux has-session -t ran 2>/dev/null"
result = subprocess.run(check_cmd, shell=True, capture_output=True)
if result.returncode == 0:
# Capture tmux output
cmd = "tmux capture-pane -t ran -p"
output = subprocess.check_output(cmd, shell=True, text=True)
with tmux_lock:
# Keep only the last 100 lines to avoid memory issues
lines = output.split('\n')
tmux_output[:] = lines[-100:] if len(lines) > 100 else lines
else:
with tmux_lock:
tmux_output[:] = ["TMUX session 'ran' not found. Please start the RAN application."]
time.sleep(1) # Update every second
except Exception as e:
print(f"Error capturing tmux output: {e}")
with tmux_lock:
tmux_output[:] = [f"Error capturing tmux output: {str(e)}"]
time.sleep(5) # Wait longer if there's an error
def start_tmux_capture():
"""Start the tmux capture thread"""
global tmux_thread
tmux_stop_event.clear()
if tmux_thread is None or not tmux_thread.is_alive():
tmux_thread = threading.Thread(target=capture_tmux_output, daemon=True)
tmux_thread.start()
print("TMUX capture thread started")
return True
def stop_tmux_capture():
"""Stop the tmux capture thread"""
global tmux_thread
tmux_stop_event.set()
if tmux_thread and tmux_thread.is_alive():
tmux_thread.join(timeout=2.0)
print("TMUX capture thread stopped")
return True
# ----------------- Gain Updates ----------------- # ----------------- 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
@ -262,22 +186,19 @@ def generate_spectrum_plot():
else: else:
iq_sample = np.pad(iq_all, (window_samples - len(iq_all), 0)) iq_sample = np.pad(iq_all, (window_samples - len(iq_all), 0))
# Create plot with optimized settings # Create plot
plt.rcParams['savefig.dpi'] = 80 fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(12, 6))
plt.rcParams['figure.dpi'] = 80 fig.subplots_adjust(hspace=0.4)
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(10, 5))
fig.subplots_adjust(hspace=0.3)
# Time-domain plot # Time-domain plot
times_ms = np.arange(len(iq_sample)) * 1000 / sample_rate times_ms = np.arange(len(iq_sample)) * 1000 / sample_rate
ax1.plot(times_ms, np.real(iq_sample), label="Real", color='b', linewidth=0.8) ax1.plot(times_ms, np.real(iq_sample), label="Real", color='b')
ax1.plot(times_ms, np.imag(iq_sample), label="Imag", color='r', linewidth=0.8) ax1.plot(times_ms, np.imag(iq_sample), label="Imag", color='r')
ax1.set_xlim(0, window_ms) ax1.set_xlim(0, window_ms)
ax1.set_xlabel("Time (ms)") ax1.set_xlabel("Time (ms)")
ax1.set_ylabel("IQ Amplitude") ax1.set_ylabel("IQ Amplitude")
ax1.grid(True, which='both', linestyle='--', linewidth=0.5) ax1.grid(True, which='both', linestyle='--', linewidth=0.5)
ax1.legend(fontsize=8) ax1.legend()
# Spectrogram # Spectrogram
cmap = plt.get_cmap('twilight') cmap = plt.get_cmap('twilight')
@ -299,60 +220,25 @@ def generate_spectrum_plot():
) )
ax2.xaxis.set_minor_locator(ticker.AutoMinorLocator()) ax2.xaxis.set_minor_locator(ticker.AutoMinorLocator())
# Save to buffer plt.savefig(PLOT_PATH, bbox_inches='tight')
buf = io.BytesIO()
plt.savefig(buf, format='png', bbox_inches='tight', dpi=80)
buf.seek(0)
# Store in global buffer and send via WebSocket
with plot_lock:
plot_buffer.seek(0)
plot_buffer.truncate()
plot_buffer.write(buf.getvalue())
plot_buffer.seek(0)
# Send via SocketIO
img_data = base64.b64encode(buf.getvalue()).decode('utf-8')
socketio.emit('plot_update', {'image': img_data})
plt.close(fig) plt.close(fig)
buf.close()
except zmq.Again: except zmq.Again:
# No new data # No new data
fig, ax = plt.subplots(figsize=(10, 5)) fig, ax = plt.subplots(figsize=(12, 6))
ax.text(0.5, 0.5, "Waiting for data...", ax.text(0.5, 0.5, "Waiting for data...",
ha='center', va='center', transform=ax.transAxes, fontsize=14) ha='center', va='center', transform=ax.transAxes, fontsize=16)
ax.set_title("Spectrum Analyzer - No Data (Streaming Active)") ax.set_title("Spectrum Analyzer - No Data (Streaming Active)")
plt.savefig(PLOT_PATH, bbox_inches='tight')
buf = io.BytesIO()
plt.savefig(buf, format='png', bbox_inches='tight', dpi=80)
buf.seek(0)
with plot_lock:
plot_buffer.seek(0)
plot_buffer.truncate()
plot_buffer.write(buf.getvalue())
plt.close(fig) plt.close(fig)
buf.close()
except Exception as e: except Exception as e:
print(f"Plot generation error: {e}") print(f"Plot generation error: {e}")
fig, ax = plt.subplots(figsize=(10, 5)) fig, ax = plt.subplots(figsize=(12, 6))
ax.text(0.5, 0.5, f"Error: {str(e)}", ax.text(0.5, 0.5, f"Error: {str(e)}",
ha='center', va='center', transform=ax.transAxes, fontsize=12) ha='center', va='center', transform=ax.transAxes, fontsize=12)
ax.set_title("Spectrum Analyzer - Error") ax.set_title("Spectrum Analyzer - Error")
plt.savefig(PLOT_PATH, bbox_inches='tight')
buf = io.BytesIO()
plt.savefig(buf, format='png', bbox_inches='tight', dpi=80)
buf.seek(0)
with plot_lock:
plot_buffer.seek(0)
plot_buffer.truncate()
plot_buffer.write(buf.getvalue())
plt.close(fig) plt.close(fig)
buf.close()
time.sleep(0.1) time.sleep(0.1)
@ -391,22 +277,12 @@ def stop_plotting():
plot_thread.join(timeout=2.0) plot_thread.join(timeout=2.0)
# Create stopped message plot # Create stopped message plot
fig, ax = plt.subplots(figsize=(10, 5)) fig, ax = plt.subplots(figsize=(12, 6))
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, fontsize=14) ha='center', va='center', transform=ax.transAxes, fontsize=16)
ax.set_title("Spectrum Analyzer - Stopped") ax.set_title("Spectrum Analyzer - Stopped")
plt.savefig(PLOT_PATH, bbox_inches='tight')
buf = io.BytesIO()
plt.savefig(buf, format='png', bbox_inches='tight', dpi=80)
buf.seek(0)
with plot_lock:
plot_buffer.seek(0)
plot_buffer.truncate()
plot_buffer.write(buf.getvalue())
plt.close(fig) plt.close(fig)
buf.close()
print("Plotting thread stopped") print("Plotting thread stopped")
return True return True
@ -418,7 +294,7 @@ def pause_plotting():
if pause_event.is_set(): if pause_event.is_set():
pause_event.clear() pause_event.clear()
print("Plotting resumed") print("Plotting resumed")
return "running" return "resumed"
else: else:
pause_event.set() pause_event.set()
print("Plotting paused") print("Plotting paused")
@ -427,11 +303,7 @@ def pause_plotting():
# ----------------- Flask Routes ----------------- # ----------------- Flask Routes -----------------
@app.route('/') @app.route('/')
def index(): def index():
# Read the template file and inject Socket.IO script
template_path = 'templates/index.html'
if os.path.exists(template_path):
return render_template('index.html') return render_template('index.html')
return "Template not found", 404
@app.route('/update_gains', methods=['POST']) @app.route('/update_gains', methods=['POST'])
def update_gains(): def update_gains():
@ -464,32 +336,9 @@ def update_gains():
@app.route('/plot') @app.route('/plot')
def plot(): def plot():
try: try:
with plot_lock:
plot_buffer.seek(0)
img_data = plot_buffer.read()
if not img_data:
# Return placeholder if buffer is empty
return send_file(PLOT_PATH, mimetype='image/png') return send_file(PLOT_PATH, mimetype='image/png')
# Create a new BytesIO object for this request
img_io = io.BytesIO(img_data)
img_io.seek(0)
response = send_file(
img_io,
mimetype='image/png',
cache_timeout=0
)
response.headers['Cache-Control'] = 'no-cache, no-store, must-revalidate'
response.headers['Pragma'] = 'no-cache'
response.headers['Expires'] = '0'
return response
except Exception as e: except Exception as e:
print(f"Error serving plot: {e}")
try:
return send_file(PLOT_PATH, mimetype='image/png') return send_file(PLOT_PATH, mimetype='image/png')
except:
return "Error serving plot", 500
@app.route('/get_gains') @app.route('/get_gains')
def get_gains(): def get_gains():
@ -542,12 +391,10 @@ def start_stream():
try: try:
success = start_plotting() success = start_plotting()
if success: if success:
start_tmux_capture()
return jsonify({"status": "success", "message": "Streaming started"}) return jsonify({"status": "success", "message": "Streaming started"})
else: else:
return jsonify({"status": "error", "message": "Failed to start streaming"}), 500 return jsonify({"status": "error", "message": "Failed to start streaming"}), 500
except Exception as e: except Exception as e:
print(f"Error starting stream: {e}")
return jsonify({"status": "error", "message": f"Error: {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'])
@ -555,12 +402,10 @@ def stop_stream():
try: try:
success = stop_plotting() success = stop_plotting()
if success: if success:
stop_tmux_capture()
return jsonify({"status": "success", "message": "Streaming stopped"}) return jsonify({"status": "success", "message": "Streaming stopped"})
else: else:
return jsonify({"status": "error", "message": "Failed to stop streaming"}), 500 return jsonify({"status": "error", "message": "Failed to stop streaming"}), 500
except Exception as e: except Exception as e:
print(f"Error stopping stream: {e}")
return jsonify({"status": "error", "message": f"Error: {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'])
@ -569,7 +414,6 @@ def pause_stream():
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:
print(f"Error pausing stream: {e}")
return jsonify({"status": "error", "message": f"Error: {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', methods=['GET'])
@ -586,21 +430,6 @@ def get_stream_state():
return jsonify({"state": state}) return jsonify({"state": state})
@app.route('/tmux_output', methods=['GET'])
def get_tmux_output():
"""Return the captured tmux output"""
with tmux_lock:
return jsonify({"output": tmux_output})
# WebSocket event handlers
@socketio.on('connect')
def handle_connect():
print('Client connected via WebSocket')
@socketio.on('disconnect')
def handle_disconnect():
print('Client disconnected from WebSocket')
def save_config(): def save_config():
with config_lock: with config_lock:
cfg = dict(config) cfg = dict(config)
@ -615,16 +444,14 @@ def save_config():
def main(): def main():
# Ensure placeholder image exists # Ensure placeholder image exists
if not os.path.exists(PLOT_PATH): if not os.path.exists(PLOT_PATH):
fig, ax = plt.subplots(figsize=(10, 5)) fig, ax = plt.subplots(figsize=(12, 6))
ax.text(0.5, 0.5, "Click Start to begin streaming", ha='center', va='center', fontsize=14) ax.text(0.5, 0.5, "Click Start to begin streaming", ha='center', va='center', fontsize=16)
ax.set_title("Gain-Viz Spectrum Analyzer - Ready") ax.set_title("Gain-Viz Spectrum Analyzer - Ready")
plt.savefig(PLOT_PATH, bbox_inches='tight', dpi=80) plt.savefig(PLOT_PATH)
plt.close(fig) plt.close(fig)
print("Gain-Viz server starting on http://0.0.0.0:5000") print("Gain-Viz server started. Use the web interface to control streaming.")
print("WebSocket support enabled") app.run(host="0.0.0.0", port=5000, debug=True, use_reloader=False)
# Run the SocketIO server
socketio.run(app, host="0.0.0.0", port=5000, debug=False, use_reloader=False, allow_unsafe_werkzeug=True)
if __name__ == '__main__': if __name__ == '__main__':
main() main()

Binary file not shown.

Before

Width:  |  Height:  |  Size: 414 KiB

View File

@ -20,8 +20,6 @@
--success: #10b981; --success: #10b981;
--warning: #f59e0b; --warning: #f59e0b;
--error: #ef4444; --error: #ef4444;
--terminal-bg: #1e293b;
--terminal-text: #e2e8f0;
} }
* { * {
@ -40,7 +38,7 @@
} }
.container { .container {
max-width: 1600px; max-width: 1400px;
margin: 0 auto; margin: 0 auto;
background: var(--card); background: var(--card);
border-radius: var(--radius); border-radius: var(--radius);
@ -380,28 +378,21 @@
border: 1px solid #fed7aa; border: 1px solid #fed7aa;
} }
/* Plot area - OPTIMIZED FOR MAXIMUM SPACE */ /* Plot area */
.visualization-panel {
display: flex;
flex-direction: column;
height: 100%;
}
.plot-area { .plot-area {
flex: 1; flex: 1;
min-height: 0; padding: 24px;
padding: 16px;
background: #f8fafc; background: #f8fafc;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
overflow: hidden;
} }
.plot-header { .plot-header {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
margin-bottom: 12px; margin-bottom: 16px;
flex-shrink: 0;
} }
.plot-header h3 { .plot-header h3 {
@ -425,105 +416,25 @@
} }
.plot-card { .plot-card {
flex: 1;
min-height: 0;
background: #fff; background: #fff;
border: 1px solid var(--border); border: 1px solid var(--border);
border-radius: var(--radius); border-radius: var(--radius);
padding: 4px; padding: 16px;
flex: 1;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
overflow: hidden; overflow: hidden;
box-shadow: var(--shadow); box-shadow: var(--shadow);
position: relative;
} }
#plotImage { #plotImage {
position: absolute;
top: 0;
left: 0;
width: 100%; width: 100%;
height: 100%; height: 100%;
object-fit: contain; object-fit: contain;
border-radius: 8px; border-radius: 8px;
display: block; display: block;
background: #f8fafc; background: #f8fafc;
image-rendering: -webkit-optimize-contrast; border: 1px solid var(--border);
}
/* TMUX Terminal - COMPACT */
.tmux-container {
height: 150px;
margin-top: 8px;
display: flex;
flex-direction: column;
flex-shrink: 0;
}
.tmux-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 4px 10px;
background: var(--terminal-bg);
color: var(--terminal-text);
border-top-left-radius: var(--radius);
border-top-right-radius: var(--radius);
font-size: 0.8rem;
font-weight: 500;
flex-shrink: 0;
}
.tmux-controls {
display: flex;
gap: 4px;
}
.tmux-btn {
background: transparent;
border: none;
color: var(--terminal-text);
cursor: pointer;
padding: 2px 4px;
border-radius: 3px;
font-size: 0.7rem;
}
.tmux-btn:hover {
background: rgba(255, 255, 255, 0.1);
}
.tmux-terminal {
flex: 1;
background: var(--terminal-bg);
color: var(--terminal-text);
font-family: 'Courier New', monospace;
font-size: 0.75rem;
padding: 6px;
overflow-y: auto;
border-bottom-left-radius: var(--radius);
border-bottom-right-radius: var(--radius);
white-space: pre-wrap;
word-wrap: break-word;
line-height: 1.2;
}
.tmux-terminal::-webkit-scrollbar {
width: 4px;
}
.tmux-terminal::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.05);
border-radius: 2px;
}
.tmux-terminal::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.2);
border-radius: 2px;
}
.tmux-terminal::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.3);
} }
footer { footer {
@ -567,7 +478,7 @@
} }
.controls-panel, .plot-area { .controls-panel, .plot-area {
padding: 12px; padding: 16px;
} }
.gain-item { .gain-item {
@ -616,7 +527,6 @@
background: #94a3b8; background: #94a3b8;
} }
</style> </style>
<script src="https://cdn.socket.io/4.5.4/socket.io.min.js"></script>
</head> </head>
<body> <body>
<div class="container"> <div class="container">
@ -769,18 +679,6 @@
<div class="plot-card"> <div class="plot-card">
<img id="plotImage" src="/plot" alt="Spectrum Analysis plot"> <img id="plotImage" src="/plot" alt="Spectrum Analysis plot">
</div> </div>
<!-- TMUX Terminal -->
<div class="tmux-container">
<div class="tmux-header">
<span>TMUX Output (ran)</span>
<div class="tmux-controls">
<button class="tmux-btn" id="clearTmuxBtn">Clear</button>
<button class="tmux-btn" id="autoScrollTmuxBtn">Auto Scroll: ON</button>
</div>
</div>
<div class="tmux-terminal" id="tmuxTerminal"></div>
</div>
</div> </div>
</div> </div>
</div> </div>
@ -793,14 +691,9 @@
<script> <script>
(function(){ (function(){
const plotImg = document.getElementById('plotImage'); const plotImg = document.getElementById('plotImage');
const tmuxTerminal = document.getElementById('tmuxTerminal');
let isPlotPaused = false; let isPlotPaused = false;
let isStreaming = false; let isStreaming = false;
let autoScroll = true; let refreshInterval;
let lastTmuxLength = 0;
// Socket.IO connection for real-time updates
const socket = io();
// Helper to show status for a specific element // Helper to show status for a specific element
function showStatus(id, message, type = 'success') { function showStatus(id, message, type = 'success') {
@ -850,32 +743,10 @@
stopBtn.disabled = true; stopBtn.disabled = true;
isStreaming = false; isStreaming = false;
isPlotPaused = false; isPlotPaused = false;
tmuxTerminal.textContent = '';
break; break;
} }
} }
// Socket.IO event handlers (FIXED)
socket.on('connect', function() {
console.log('WebSocket connected');
});
socket.on('disconnect', function() {
console.log('WebSocket disconnected');
setTimeout(function() {
window.location.reload();
}, 5000);
});
socket.on('plot_update', function(data) {
// Update plot with base64 image data
plotImg.src = 'data:image/png;base64,' + data.image;
});
socket.on('connect_error', function(error) {
console.error('WebSocket error:', error);
});
// Stream control functions // Stream control functions
async function startStream() { async function startStream() {
try { try {
@ -939,33 +810,13 @@
} }
} }
// Fetch TMUX output // ---------------- Gain Form ----------------
async function fetchTmuxOutput() {
try {
const response = await fetch('/tmux_output');
const data = await response.json();
if (data.output && data.output.length > 0) {
const currentOutput = data.output.join('\n');
if (currentOutput.length !== tmuxTerminal.textContent.length) {
tmuxTerminal.textContent = currentOutput;
if (autoScroll) {
tmuxTerminal.scrollTop = tmuxTerminal.scrollHeight;
}
}
}
} catch (err) {
console.error('Error fetching TMUX output:', err);
}
}
// Gain Form
const gainForm = document.getElementById('gainForm'); const gainForm = document.getElementById('gainForm');
const toggles = document.querySelectorAll('.gain-toggle'); const toggles = document.querySelectorAll('.gain-toggle');
const gainUpdateBtn = document.getElementById('gainUpdateBtn'); const gainUpdateBtn = document.getElementById('gainUpdateBtn');
const gainRefreshBtn = document.getElementById('gainRefreshBtn'); const gainRefreshBtn = document.getElementById('gainRefreshBtn');
// enable/disable inputs depending on toggle
toggles.forEach(t => { toggles.forEach(t => {
const inputId = t.dataset.input; const inputId = t.dataset.input;
const inputEl = document.getElementById(inputId); const inputEl = document.getElementById(inputId);
@ -977,6 +828,7 @@
}); });
}); });
// load existing gain values from server and populate inputs
function loadGains(){ function loadGains(){
fetch('/get_gains') fetch('/get_gains')
.then(r => r.json()) .then(r => r.json())
@ -986,6 +838,7 @@
if (data.usrp_rx_gain !== undefined) document.getElementById('usrp_rx_gain').value = data.usrp_rx_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_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; 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 => { toggles.forEach(t => {
t.checked = false; t.checked = false;
const input = document.getElementById(t.dataset.input); const input = document.getElementById(t.dataset.input);
@ -1018,11 +871,13 @@
gainUpdateBtn.disabled = false; gainUpdateBtn.disabled = false;
if (data && data.status === 'success') { if (data && data.status === 'success') {
showStatus('gainStatusMessage', data.message || 'Gains updated', 'success'); showStatus('gainStatusMessage', data.message || 'Gains updated', 'success');
// reset toggles
toggles.forEach(t => { toggles.forEach(t => {
t.checked = false; t.checked = false;
const input = document.getElementById(t.dataset.input); const input = document.getElementById(t.dataset.input);
if (input) input.disabled = true; if (input) input.disabled = true;
}); });
// reload gains from server to reflect current state
loadGains(); loadGains();
} else { } else {
showStatus('gainStatusMessage', data.message || 'Error updating gains', 'error'); showStatus('gainStatusMessage', data.message || 'Error updating gains', 'error');
@ -1040,7 +895,7 @@
showStatus('gainStatusMessage','Gains reloaded','success'); showStatus('gainStatusMessage','Gains reloaded','success');
}); });
// Params Form // ---------------- Params Form ----------------
const paramForm = document.getElementById('paramForm'); const paramForm = document.getElementById('paramForm');
paramForm.addEventListener('submit', function(e){ paramForm.addEventListener('submit', function(e){
e.preventDefault(); e.preventDefault();
@ -1064,19 +919,21 @@
}); });
}); });
// Plot refresh // ---------------- Plot refresh ----------------
document.getElementById('refreshPlotBtn').addEventListener('click', function() { function refreshPlot(){
if (!isStreaming || isPlotPaused) return;
// use cache-buster to avoid cached/partial file
plotImg.src = '/plot?_ts=' + Date.now(); plotImg.src = '/plot?_ts=' + Date.now();
}); }
// TMUX controls // Start auto refresh only when streaming
document.getElementById('clearTmuxBtn').addEventListener('click', function() { function startAutoRefresh() {
tmuxTerminal.textContent = ''; refreshInterval = setInterval(refreshPlot, 500);
}); }
document.getElementById('autoScrollTmuxBtn').addEventListener('click', function() { // Manual refresh button
autoScroll = !autoScroll; document.getElementById('refreshPlotBtn').addEventListener('click', function() {
this.textContent = `Auto Scroll: ${autoScroll ? 'ON' : 'OFF'}`; refreshPlot();
}); });
// Stream control buttons // Stream control buttons
@ -1087,12 +944,10 @@
// Check stream state every 2 seconds // Check stream state every 2 seconds
setInterval(checkStreamState, 2000); setInterval(checkStreamState, 2000);
// Start TMUX output refresh // initial load
setInterval(fetchTmuxOutput, 1000);
// Initial load
loadGains(); loadGains();
checkStreamState(); checkStreamState();
startAutoRefresh();
})(); })();
</script> </script>
</body> </body>

View File

BIN
plot.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 415 KiB

View File

@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "gain_viz" name = "gain_viz"
version = "0.1.1" version = "0.1.0"
description = "Interactive srsRAN_Project gnb gain control and spectrum visualization tool" description = "Interactive srsRAN_Project gnb gain control and spectrum visualization tool"
authors = [ authors = [
{ name = "Qoherent Inc.", email = "info@qoherent.ai" } { name = "Qoherent Inc.", email = "info@qoherent.ai" }
@ -36,11 +36,7 @@ dependencies = [
"matplotlib", "matplotlib",
"numpy", "numpy",
"pyzmq", "pyzmq",
"pyserial", "pyserial"
"flask_socketio",
"PIL",
"io"
] ]
[project.scripts] [project.scripts]