tmux integrated
This commit is contained in:
parent
800541a141
commit
8bfea14fdd
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
|
||||
}
|
||||
|
|
@ -8,6 +8,7 @@ import threading
|
|||
import time
|
||||
import serial
|
||||
import json
|
||||
import subprocess
|
||||
|
||||
app = Flask(__name__)
|
||||
PLOT_PATH = os.path.join(os.getcwd(), "plot.png")
|
||||
|
|
@ -38,6 +39,12 @@ plot_thread = None
|
|||
stop_event = threading.Event()
|
||||
pause_event = threading.Event()
|
||||
|
||||
# TMUX output capture
|
||||
tmux_output = []
|
||||
tmux_lock = threading.Lock()
|
||||
tmux_thread = None
|
||||
tmux_stop_event = threading.Event()
|
||||
|
||||
# ----------------- Serial / SCM -----------------
|
||||
def connect_serial(port, baudrate=115200, timeout=1):
|
||||
"""Connect to a serial port with even parity."""
|
||||
|
|
@ -89,6 +96,60 @@ def scm_conf(port, baudrate, rx_cmd, tx_cmd):
|
|||
return True
|
||||
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 -----------------
|
||||
def gain_update(usrp_tx, usrp_rx, scm_tx, scm_rx):
|
||||
global usrp_tx_gain, usrp_rx_gain, scm_tx_gain, scm_rx_gain
|
||||
|
|
@ -391,6 +452,7 @@ def start_stream():
|
|||
try:
|
||||
success = start_plotting()
|
||||
if success:
|
||||
start_tmux_capture() # Start capturing tmux output
|
||||
return jsonify({"status": "success", "message": "Streaming started"})
|
||||
else:
|
||||
return jsonify({"status": "error", "message": "Failed to start streaming"}), 500
|
||||
|
|
@ -402,6 +464,7 @@ def stop_stream():
|
|||
try:
|
||||
success = stop_plotting()
|
||||
if success:
|
||||
stop_tmux_capture() # Stop capturing tmux output
|
||||
return jsonify({"status": "success", "message": "Streaming stopped"})
|
||||
else:
|
||||
return jsonify({"status": "error", "message": "Failed to stop streaming"}), 500
|
||||
|
|
@ -430,6 +493,12 @@ def get_stream_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})
|
||||
|
||||
def save_config():
|
||||
with config_lock:
|
||||
cfg = dict(config)
|
||||
|
|
|
|||
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;
|
||||
--warning: #f59e0b;
|
||||
--error: #ef4444;
|
||||
--terminal-bg: #1e293b;
|
||||
--terminal-text: #e2e8f0;
|
||||
}
|
||||
|
||||
* {
|
||||
|
|
@ -38,7 +40,7 @@
|
|||
}
|
||||
|
||||
.container {
|
||||
max-width: 1400px;
|
||||
max-width: 1600px;
|
||||
margin: 0 auto;
|
||||
background: var(--card);
|
||||
border-radius: var(--radius);
|
||||
|
|
@ -378,21 +380,28 @@
|
|||
border: 1px solid #fed7aa;
|
||||
}
|
||||
|
||||
/* Plot area */
|
||||
/* Plot area - OPTIMIZED FOR MAXIMUM SPACE */
|
||||
.visualization-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.plot-area {
|
||||
flex: 1;
|
||||
padding: 24px;
|
||||
min-height: 0;
|
||||
padding: 16px; /* Reduced padding */
|
||||
background: #f8fafc;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.plot-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
margin-bottom: 12px; /* Reduced margin */
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.plot-header h3 {
|
||||
|
|
@ -416,25 +425,104 @@
|
|||
}
|
||||
|
||||
.plot-card {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
background: #fff;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
padding: 16px;
|
||||
flex: 1;
|
||||
padding: 4px; /* Minimal padding */
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
box-shadow: var(--shadow);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#plotImage {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
border-radius: 8px;
|
||||
display: block;
|
||||
background: #f8fafc;
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
/* TMUX Terminal - COMPACT */
|
||||
.tmux-container {
|
||||
height: 150px; /* Further reduced */
|
||||
margin-top: 8px; /* Minimal margin */
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.tmux-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 4px 10px; /* Minimal padding */
|
||||
background: var(--terminal-bg);
|
||||
color: var(--terminal-text);
|
||||
border-top-left-radius: var(--radius);
|
||||
border-top-right-radius: var(--radius);
|
||||
font-size: 0.8rem; /* Smaller font */
|
||||
font-weight: 500;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.tmux-controls {
|
||||
display: flex;
|
||||
gap: 4px; /* Minimal gap */
|
||||
}
|
||||
|
||||
.tmux-btn {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--terminal-text);
|
||||
cursor: pointer;
|
||||
padding: 2px 4px; /* Minimal padding */
|
||||
border-radius: 3px;
|
||||
font-size: 0.7rem; /* Smaller font */
|
||||
}
|
||||
|
||||
.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; /* Smaller font */
|
||||
padding: 6px; /* Minimal padding */
|
||||
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; /* Tighter line height */
|
||||
}
|
||||
|
||||
.tmux-terminal::-webkit-scrollbar {
|
||||
width: 4px; /* Thinner scrollbar */
|
||||
}
|
||||
|
||||
.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 {
|
||||
|
|
@ -478,7 +566,7 @@
|
|||
}
|
||||
|
||||
.controls-panel, .plot-area {
|
||||
padding: 16px;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.gain-item {
|
||||
|
|
@ -679,6 +767,18 @@
|
|||
<div class="plot-card">
|
||||
<img id="plotImage" src="/plot" alt="Spectrum Analysis plot">
|
||||
</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>
|
||||
|
|
@ -691,9 +791,13 @@
|
|||
<script>
|
||||
(function(){
|
||||
const plotImg = document.getElementById('plotImage');
|
||||
const tmuxTerminal = document.getElementById('tmuxTerminal');
|
||||
let isPlotPaused = false;
|
||||
let isStreaming = false;
|
||||
let refreshInterval;
|
||||
let tmuxInterval;
|
||||
let autoScroll = true;
|
||||
let lastTmuxLength = 0;
|
||||
|
||||
// Helper to show status for a specific element
|
||||
function showStatus(id, message, type = 'success') {
|
||||
|
|
@ -743,6 +847,7 @@
|
|||
stopBtn.disabled = true;
|
||||
isStreaming = false;
|
||||
isPlotPaused = false;
|
||||
tmuxTerminal.textContent = ''; // Clear terminal when stopped
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
@ -810,6 +915,29 @@
|
|||
}
|
||||
}
|
||||
|
||||
// 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) {
|
||||
// Only update if new content is available
|
||||
const currentOutput = data.output.join('\n');
|
||||
if (currentOutput.length !== tmuxTerminal.textContent.length) {
|
||||
tmuxTerminal.textContent = currentOutput;
|
||||
|
||||
// Auto-scroll to bottom if enabled
|
||||
if (autoScroll) {
|
||||
tmuxTerminal.scrollTop = tmuxTerminal.scrollHeight;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error fetching TMUX output:', err);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------- Gain Form ----------------
|
||||
const gainForm = document.getElementById('gainForm');
|
||||
const toggles = document.querySelectorAll('.gain-toggle');
|
||||
|
|
@ -931,11 +1059,26 @@
|
|||
refreshInterval = setInterval(refreshPlot, 500);
|
||||
}
|
||||
|
||||
// Start TMUX output refresh
|
||||
function startTmuxRefresh() {
|
||||
tmuxInterval = setInterval(fetchTmuxOutput, 1000); // Update every second
|
||||
}
|
||||
|
||||
// Manual refresh button
|
||||
document.getElementById('refreshPlotBtn').addEventListener('click', function() {
|
||||
refreshPlot();
|
||||
});
|
||||
|
||||
// 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
|
||||
document.getElementById('startBtn').addEventListener('click', startStream);
|
||||
document.getElementById('stopBtn').addEventListener('click', stopStream);
|
||||
|
|
@ -948,6 +1091,7 @@
|
|||
loadGains();
|
||||
checkStreamState();
|
||||
startAutoRefresh();
|
||||
startTmuxRefresh();
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
|
|
|
|||
0
gain_viz/tmux_ran.log
Normal file
0
gain_viz/tmux_ran.log
Normal file
Loading…
Reference in New Issue
Block a user