Compare commits
6 Commits
main
...
tmux-integ
| Author | SHA1 | Date | |
|---|---|---|---|
|
Ggael
|
6ff078711d | ||
|
Ggael
|
b904947483 | ||
|
Ggael
|
ee27d5cc7c | ||
|
Ggael
|
dbaf222ebc | ||
|
Ggael
|
2e8a358dd2 | ||
|
Ggael
|
8bfea14fdd |
|
|
@ -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.0-py3-none-any.whl
|
pip install dist/gain_viz-0.1.1-py3-none-any.whl
|
||||||
export PATH=$PATH:~/.local/bin
|
export PATH=$PATH:~/.local/bin
|
||||||
source ~/.bashrc
|
source ~/.bashrc
|
||||||
|
|
||||||
|
|
|
||||||
BIN
dist/gain_viz-0.1.0-py3-none-any.whl
vendored
Normal file
BIN
dist/gain_viz-0.1.0-py3-none-any.whl
vendored
Normal file
Binary file not shown.
BIN
dist/gain_viz-0.1.0.tar.gz
vendored
Normal file
BIN
dist/gain_viz-0.1.0.tar.gz
vendored
Normal file
Binary file not shown.
72
gain_viz.egg-info/PKG-INFO
Normal file
72
gain_viz.egg-info/PKG-INFO
Normal file
|
|
@ -0,0 +1,72 @@
|
||||||
|
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
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
|
# 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.
|
||||||
12
gain_viz.egg-info/SOURCES.txt
Normal file
12
gain_viz.egg-info/SOURCES.txt
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
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
|
||||||
1
gain_viz.egg-info/dependency_links.txt
Normal file
1
gain_viz.egg-info/dependency_links.txt
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
|
||||||
2
gain_viz.egg-info/entry_points.txt
Normal file
2
gain_viz.egg-info/entry_points.txt
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
[console_scripts]
|
||||||
|
gain_viz = gain_viz.app:main
|
||||||
5
gain_viz.egg-info/requires.txt
Normal file
5
gain_viz.egg-info/requires.txt
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
flask
|
||||||
|
matplotlib
|
||||||
|
numpy
|
||||||
|
pyzmq
|
||||||
|
pyserial
|
||||||
1
gain_viz.egg-info/top_level.txt
Normal file
1
gain_viz.egg-info/top_level.txt
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
gain_viz
|
||||||
12
gain_viz.json
Normal file
12
gain_viz.json
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
{
|
||||||
|
"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
|
||||||
|
}
|
||||||
219
gain_viz/app.py
219
gain_viz/app.py
|
|
@ -8,9 +8,18 @@ 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__)
|
||||||
PLOT_PATH = os.path.join(os.getcwd(), "plot.png")
|
# Initialize SocketIO with proper configuration
|
||||||
|
socketio = SocketIO(app, cors_allowed_origins="*", async_mode='threading')
|
||||||
|
|
||||||
# ----------------- Shared Config -----------------
|
# ----------------- Shared Config -----------------
|
||||||
config = {
|
config = {
|
||||||
|
|
@ -23,7 +32,7 @@ config = {
|
||||||
"center_freq": 3.415e9,
|
"center_freq": 3.415e9,
|
||||||
"NFFT": 1024,
|
"NFFT": 1024,
|
||||||
"tcp_port": 5556,
|
"tcp_port": 5556,
|
||||||
"streaming": False, # Added streaming state
|
"streaming": False,
|
||||||
}
|
}
|
||||||
config_lock = threading.Lock()
|
config_lock = threading.Lock()
|
||||||
|
|
||||||
|
|
@ -38,6 +47,19 @@ 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."""
|
||||||
|
|
@ -89,6 +111,60 @@ 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
|
||||||
|
|
@ -186,19 +262,22 @@ 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
|
# Create plot with optimized settings
|
||||||
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(12, 6))
|
plt.rcParams['savefig.dpi'] = 80
|
||||||
fig.subplots_adjust(hspace=0.4)
|
plt.rcParams['figure.dpi'] = 80
|
||||||
|
|
||||||
|
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')
|
ax1.plot(times_ms, np.real(iq_sample), label="Real", color='b', linewidth=0.8)
|
||||||
ax1.plot(times_ms, np.imag(iq_sample), label="Imag", color='r')
|
ax1.plot(times_ms, np.imag(iq_sample), label="Imag", color='r', linewidth=0.8)
|
||||||
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()
|
ax1.legend(fontsize=8)
|
||||||
|
|
||||||
# Spectrogram
|
# Spectrogram
|
||||||
cmap = plt.get_cmap('twilight')
|
cmap = plt.get_cmap('twilight')
|
||||||
|
|
@ -220,25 +299,60 @@ def generate_spectrum_plot():
|
||||||
)
|
)
|
||||||
ax2.xaxis.set_minor_locator(ticker.AutoMinorLocator())
|
ax2.xaxis.set_minor_locator(ticker.AutoMinorLocator())
|
||||||
|
|
||||||
plt.savefig(PLOT_PATH, bbox_inches='tight')
|
# Save to buffer
|
||||||
|
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=(12, 6))
|
fig, ax = plt.subplots(figsize=(10, 5))
|
||||||
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=16)
|
ha='center', va='center', transform=ax.transAxes, fontsize=14)
|
||||||
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=(12, 6))
|
fig, ax = plt.subplots(figsize=(10, 5))
|
||||||
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)
|
||||||
|
|
||||||
|
|
@ -277,12 +391,22 @@ 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=(12, 6))
|
fig, ax = plt.subplots(figsize=(10, 5))
|
||||||
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=16)
|
ha='center', va='center', transform=ax.transAxes, fontsize=14)
|
||||||
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
|
||||||
|
|
@ -294,7 +418,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 "resumed"
|
return "running"
|
||||||
else:
|
else:
|
||||||
pause_event.set()
|
pause_event.set()
|
||||||
print("Plotting paused")
|
print("Plotting paused")
|
||||||
|
|
@ -303,7 +427,11 @@ 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():
|
||||||
|
|
@ -336,9 +464,32 @@ 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():
|
||||||
|
|
@ -391,10 +542,12 @@ 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'])
|
||||||
|
|
@ -402,10 +555,12 @@ 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'])
|
||||||
|
|
@ -414,6 +569,7 @@ 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'])
|
||||||
|
|
@ -430,6 +586,21 @@ 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)
|
||||||
|
|
@ -444,14 +615,16 @@ 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=(12, 6))
|
fig, ax = plt.subplots(figsize=(10, 5))
|
||||||
ax.text(0.5, 0.5, "Click Start to begin streaming", ha='center', va='center', fontsize=16)
|
ax.text(0.5, 0.5, "Click Start to begin streaming", ha='center', va='center', fontsize=14)
|
||||||
ax.set_title("Gain-Viz Spectrum Analyzer - Ready")
|
ax.set_title("Gain-Viz Spectrum Analyzer - Ready")
|
||||||
plt.savefig(PLOT_PATH)
|
plt.savefig(PLOT_PATH, bbox_inches='tight', dpi=80)
|
||||||
plt.close(fig)
|
plt.close(fig)
|
||||||
|
|
||||||
print("Gain-Viz server started. Use the web interface to control streaming.")
|
print("Gain-Viz server starting on http://0.0.0.0:5000")
|
||||||
app.run(host="0.0.0.0", port=5000, debug=True, use_reloader=False)
|
print("WebSocket support enabled")
|
||||||
|
# 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()
|
||||||
BIN
gain_viz/plot.png
Normal file
BIN
gain_viz/plot.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 414 KiB |
|
|
@ -20,6 +20,8 @@
|
||||||
--success: #10b981;
|
--success: #10b981;
|
||||||
--warning: #f59e0b;
|
--warning: #f59e0b;
|
||||||
--error: #ef4444;
|
--error: #ef4444;
|
||||||
|
--terminal-bg: #1e293b;
|
||||||
|
--terminal-text: #e2e8f0;
|
||||||
}
|
}
|
||||||
|
|
||||||
* {
|
* {
|
||||||
|
|
@ -38,7 +40,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
max-width: 1400px;
|
max-width: 1600px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
background: var(--card);
|
background: var(--card);
|
||||||
border-radius: var(--radius);
|
border-radius: var(--radius);
|
||||||
|
|
@ -378,21 +380,28 @@
|
||||||
border: 1px solid #fed7aa;
|
border: 1px solid #fed7aa;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Plot area */
|
/* Plot area - OPTIMIZED FOR MAXIMUM SPACE */
|
||||||
|
.visualization-panel {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.plot-area {
|
.plot-area {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
padding: 24px;
|
min-height: 0;
|
||||||
|
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: 16px;
|
margin-bottom: 12px;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.plot-header h3 {
|
.plot-header h3 {
|
||||||
|
|
@ -416,25 +425,105 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.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: 16px;
|
padding: 4px;
|
||||||
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;
|
||||||
border: 1px solid var(--border);
|
image-rendering: -webkit-optimize-contrast;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 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 {
|
||||||
|
|
@ -478,7 +567,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.controls-panel, .plot-area {
|
.controls-panel, .plot-area {
|
||||||
padding: 16px;
|
padding: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.gain-item {
|
.gain-item {
|
||||||
|
|
@ -527,6 +616,7 @@
|
||||||
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">
|
||||||
|
|
@ -679,6 +769,18 @@
|
||||||
<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>
|
||||||
|
|
@ -688,12 +790,17 @@
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<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 refreshInterval;
|
let autoScroll = true;
|
||||||
|
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') {
|
||||||
|
|
@ -743,10 +850,32 @@
|
||||||
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 {
|
||||||
|
|
@ -810,13 +939,33 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------- Gain Form ----------------
|
// Fetch TMUX output
|
||||||
|
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);
|
||||||
|
|
@ -828,7 +977,6 @@
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// 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())
|
||||||
|
|
@ -838,7 +986,6 @@
|
||||||
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);
|
||||||
|
|
@ -871,13 +1018,11 @@
|
||||||
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');
|
||||||
|
|
@ -895,7 +1040,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();
|
||||||
|
|
@ -919,21 +1064,19 @@
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// ---------------- Plot refresh ----------------
|
// 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() {
|
document.getElementById('refreshPlotBtn').addEventListener('click', function() {
|
||||||
refreshPlot();
|
plotImg.src = '/plot?_ts=' + Date.now();
|
||||||
|
});
|
||||||
|
|
||||||
|
// TMUX controls
|
||||||
|
document.getElementById('clearTmuxBtn').addEventListener('click', function() {
|
||||||
|
tmuxTerminal.textContent = '';
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('autoScrollTmuxBtn').addEventListener('click', function() {
|
||||||
|
autoScroll = !autoScroll;
|
||||||
|
this.textContent = `Auto Scroll: ${autoScroll ? 'ON' : 'OFF'}`;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Stream control buttons
|
// Stream control buttons
|
||||||
|
|
@ -944,11 +1087,13 @@
|
||||||
// Check stream state every 2 seconds
|
// Check stream state every 2 seconds
|
||||||
setInterval(checkStreamState, 2000);
|
setInterval(checkStreamState, 2000);
|
||||||
|
|
||||||
// initial load
|
// Start TMUX output refresh
|
||||||
|
setInterval(fetchTmuxOutput, 1000);
|
||||||
|
|
||||||
|
// Initial load
|
||||||
loadGains();
|
loadGains();
|
||||||
checkStreamState();
|
checkStreamState();
|
||||||
startAutoRefresh();
|
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
0
gain_viz/tmux_ran.log
Normal file
0
gain_viz/tmux_ran.log
Normal file
|
|
@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "gain_viz"
|
name = "gain_viz"
|
||||||
version = "0.1.0"
|
version = "0.1.1"
|
||||||
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,7 +36,11 @@ dependencies = [
|
||||||
"matplotlib",
|
"matplotlib",
|
||||||
"numpy",
|
"numpy",
|
||||||
"pyzmq",
|
"pyzmq",
|
||||||
"pyserial"
|
"pyserial",
|
||||||
|
"flask_socketio",
|
||||||
|
"PIL",
|
||||||
|
"io"
|
||||||
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.scripts]
|
[project.scripts]
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user