Files
openairinterface5g/openair1/PHY/TOOLS/imscope/imscope.cpp
T

938 lines
32 KiB
C++

/*
* Licensed to the OpenAirInterface (OAI) Software Alliance under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The OpenAirInterface Software Alliance licenses this file to You under
* the OAI Public License, Version 1.1 (the "License"); you may not use this file
* except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.openairinterface.org/?page_id=698
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*-------------------------------------------------------------------------------
* For more information about the OpenAirInterface (OAI) Software Alliance:
* contact@openairinterface.org
*/
#include "imgui.h"
#include "imgui_impl_glfw.h"
#include "imgui_impl_opengl3.h"
#include <stdio.h>
#define GL_SILENCE_DEPRECATION
#if defined(IMGUI_IMPL_OPENGL_ES2)
#include <GLES2/gl2.h>
#endif
#include <GLFW/glfw3.h> // Will drag system OpenGL headers
#include "implot.h"
#include "openair1/PHY/defs_nr_UE.h"
extern "C" {
#include "openair1/PHY/TOOLS/phy_scope_interface.h"
#include "executables/softmodem-common.h"
}
#include <iostream>
#include <vector>
#include <limits>
#include <algorithm>
#include <sstream>
#include <mutex>
#include <thread>
#define MAX_OFFSETS 14
#define NR_MAX_RB 273
#define N_SC_PER_RB NR_NB_SC_PER_RB
static std::vector<int> rb_boundaries;
void copyDataThreadSafe(void *scopeData,
enum scopeDataType type,
void *dataIn,
int elementSz,
int colSz,
int lineSz,
int offset,
metadata *meta);
bool tryLockScopeData(enum scopeDataType type, int elementSz, int colSz, int lineSz, metadata *meta);
void copyDataUnsafeWithOffset(enum scopeDataType type, void *dataIn, size_t size, size_t offset, int copy_index);
void unlockScopeData(enum scopeDataType type);
static void glfw_error_callback(int error, const char *description)
{
fprintf(stderr, "GLFW Error %d: %s\n", error, description);
}
typedef struct ImScopeData {
std::mutex write_mutex;
scopeGraphData_t *scope_graph_data;
bool is_data_ready;
metadata meta;
uint64_t time_taken_in_ns;
uint64_t time_taken_in_ns_per_offset[MAX_OFFSETS];
size_t data_copied_per_offset[MAX_OFFSETS];
} ImScopeData;
typedef struct MovingAverageTimer {
uint64_t sum = 0;
float average = 0;
float last_update_time = 0;
void UpdateAverage(float time)
{
if (time > last_update_time + 1) {
float new_average = sum / (float)((time - last_update_time) / 1000);
average = 0.95 * average + 0.05 * new_average;
sum = 0;
}
}
void Add(uint64_t ns)
{
sum += ns;
}
} MovingAverageTimer;
MovingAverageTimer iq_procedure_timer;
static ImScopeData scope_array[EXTRA_SCOPE_TYPES];
typedef struct IQData {
std::vector<int16_t> real;
std::vector<int16_t> imag;
int16_t max_iq;
std::vector<float> power;
float max_power;
float timestamp;
int nonzero_count;
int len = 0;
metadata meta;
bool TryCollect(ImScopeData *imscope_data, float time, float epsilon)
{
if (imscope_data->is_data_ready) {
iq_procedure_timer.Add(imscope_data->time_taken_in_ns);
timestamp = time;
scopeGraphData_t *iq_header = imscope_data->scope_graph_data;
len = iq_header->lineSz;
real.reserve(len);
imag.reserve(len);
power.reserve(len);
c16_t *source = (c16_t *)(iq_header + 1);
max_iq = 0;
nonzero_count = 0;
for (auto i = 0; i < len; i++) {
real[i] = source[i].r;
imag[i] = source[i].i;
max_iq = std::max(max_iq, (int16_t)std::abs(source[i].r));
max_iq = std::max(max_iq, (int16_t)std::abs(source[i].i));
nonzero_count = std::abs(source[i].r) > epsilon || std::abs(source[i].i) > epsilon ? nonzero_count + 1 : nonzero_count;
power[i] = std::sqrt(std::pow(source[i].r, 2) + std::pow(source[i].i, 2));
}
meta = imscope_data->meta;
imscope_data->is_data_ready = false;
return true;
}
return false;
}
} IQData;
class LLRPlot {
int len = 0;
float timestamp = 0;
std::vector<int16_t> llr;
bool frozen = false;
bool next = false;
metadata meta;
public:
void Draw(float time, enum scopeDataType type, const char *label)
{
ImGui::BeginGroup();
if (ImGui::Button(frozen ? "Unfreeze" : "Freeze")) {
frozen = !frozen;
next = false;
}
if (frozen) {
ImGui::SameLine();
ImGui::BeginDisabled(next);
if (ImGui::Button("Load next histogram")) {
next = true;
}
ImGui::EndDisabled();
}
ImScopeData &scope_data = scope_array[type];
if (ImPlot::BeginPlot(label)) {
if (!frozen || next) {
if (scope_data.is_data_ready) {
iq_procedure_timer.Add(scope_data.time_taken_in_ns);
timestamp = time;
const int16_t *tmp = (int16_t *)(scope_data.scope_graph_data + 1);
len = scope_data.scope_graph_data->lineSz;
llr.reserve(len);
for (auto i = 0; i < len; i++) {
llr[i] = tmp[i];
}
meta = scope_data.meta;
scope_data.is_data_ready = false;
if (frozen) {
next = false;
}
}
}
ImPlot::PlotLine(label, llr.data(), len);
ImPlot::EndPlot();
}
std::stringstream ss;
if (meta.slot != -1) {
ss << " slot: " << meta.slot;
}
if (meta.frame != -1) {
ss << " frame: " << meta.frame;
}
if (!ss.str().empty()) {
ImGui::Text("Data for %s", ss.str().c_str());
}
ImGui::Text("Data is %.2f seconds old", time - timestamp);
ImGui::EndGroup();
}
};
class IQHist {
private:
bool frozen = false;
bool next = false;
float range = 100;
int num_bins = 33;
std::string label;
float min_nonzero_percentage = 0.9;
float epsilon = 0.0;
bool auto_adjust_range = true;
int plot_type = 0;
bool disable_scatterplot;
public:
IQHist(const char *label_, bool _disable_scatterplot = false)
{
label = label_;
disable_scatterplot = _disable_scatterplot;
};
bool ShouldReadData(void)
{
return !frozen || next;
}
float GetEpsilon(void)
{
return epsilon;
}
void Draw(IQData *iq_data, float time, bool new_data)
{
if (new_data && frozen && next) {
// Evaluate if new data matches filter settings
if (((float)iq_data->nonzero_count / (float)iq_data->len) > min_nonzero_percentage) {
next = false;
}
}
ImGui::BeginGroup();
ImGui::Checkbox("auto adjust range", &auto_adjust_range);
if (auto_adjust_range) {
if (range < iq_data->max_iq * 1.1) {
range = iq_data->max_iq * 1.1;
}
}
ImGui::BeginDisabled(auto_adjust_range);
ImGui::SameLine();
ImGui::DragFloat("Range", &range, 1, 0.1, std::numeric_limits<int16_t>::max());
ImGui::EndDisabled();
ImGui::DragInt("Number of bins", &num_bins, 1, 33, 101);
if (ImGui::Button(frozen ? "Unfreeze" : "Freeze")) {
frozen = !frozen;
next = false;
}
if (frozen) {
ImGui::SameLine();
ImGui::BeginDisabled(next);
if (ImGui::Button("Load next histogram")) {
next = true;
}
ImGui::EndDisabled();
ImGui::Text("Filter parameters");
ImGui::DragFloat("%% nonzero elements", &min_nonzero_percentage, 1, 0.0, 100);
ImGui::DragFloat("epsilon", &epsilon, 1, 0.0, 3000);
}
const char *items[] = {"Histogram", "RMS", "Scatter"};
ImGui::Combo("Select plot type", &plot_type, items, disable_scatterplot ? 2 : 3);
if (plot_type == 0) {
float x = ImGui::CalcItemWidth();
if (ImPlot::BeginPlot(label.c_str(), {x, x})) {
ImPlot::PlotHistogram2D(label.c_str(),
iq_data->real.data(),
iq_data->imag.data(),
iq_data->len,
num_bins,
num_bins,
ImPlotRect(-range, range, -range, range));
ImPlot::EndPlot();
}
} else if (plot_type == 2) {
float x = ImGui::CalcItemWidth();
if (ImPlot::BeginPlot(label.c_str(), {x, x})) {
int points_drawn = 0;
while (points_drawn < iq_data->len) {
// Limit the amount of data plotted with PlotScatter call (issue with vertices/draw call)
int points_to_draw = std::min(iq_data->len - points_drawn, 16000);
ImPlot::SetNextMarkerStyle(ImPlotMarker_Circle, 1, IMPLOT_AUTO_COL, 1);
ImPlot::PlotScatter(label.c_str(),
iq_data->real.data() + points_drawn,
iq_data->imag.data() + points_drawn,
points_to_draw);
points_drawn += points_to_draw;
}
ImPlot::EndPlot();
}
} else if (plot_type == 1) {
if (ImPlot::BeginPlot(label.c_str())) {
ImPlot::PlotLine(label.c_str(), iq_data->power.data(), iq_data->len);
ImPlot::EndPlot();
}
}
ImGui::Text("Maximum value = %d, nonzero elements/total %d/%d", iq_data->max_iq, iq_data->nonzero_count, iq_data->len);
ImGui::Text("Data is %.2f seconds old", time - iq_data->timestamp);
std::stringstream ss;
if (iq_data->meta.slot != -1) {
ss << " slot: " << iq_data->meta.slot;
}
if (iq_data->meta.frame != -1) {
ss << " frame: " << iq_data->meta.frame;
}
if (!ss.str().empty()) {
ImGui::Text("Data for %s", ss.str().c_str());
}
ImGui::EndGroup();
}
};
class IQSlotHeatmap {
private:
bool frozen = false;
bool next = false;
float timestamp = 0;
std::vector<float> power;
ImScopeData *scope_data;
std::string label;
int len = 0;
float max = 0;
float stop_at_min = 1000;
public:
IQSlotHeatmap(ImScopeData *scope_data_, const char *label_)
{
scope_data = scope_data_;
label = label_;
};
// Read in the data from the sink and transform it for the use by the scope
void ReadData(float time, int ofdm_symbol_size, int num_symbols, int first_carrier_offset, int num_rb)
{
auto num_sc = num_rb * NR_NB_SC_PER_RB;
if (!frozen || next) {
if (scope_data->is_data_ready) {
iq_procedure_timer.Add(scope_data->time_taken_in_ns);
uint16_t first_sc = first_carrier_offset;
uint16_t last_sc = first_sc + num_rb * NR_NB_SC_PER_RB;
bool wrapped = false;
uint16_t wrapped_first_sc = 0;
uint16_t wrapped_last_sc = 0;
if (last_sc >= ofdm_symbol_size) {
last_sc = ofdm_symbol_size - 1;
wrapped = true;
auto num_sc_left = num_sc - (last_sc - first_sc + 1);
wrapped_last_sc = wrapped_first_sc + num_sc_left - 1;
}
timestamp = time;
scopeGraphData_t *iq_header = scope_data->scope_graph_data;
len = iq_header->lineSz;
c16_t *source = (c16_t *)(iq_header + 1);
power.reserve(num_sc * num_symbols);
for (auto symbol = 0; symbol < num_symbols; symbol++) {
int subcarrier = 0;
for (auto sc = first_sc; sc <= last_sc; sc++) {
auto source_index = sc + symbol * ofdm_symbol_size;
power[subcarrier * num_symbols + symbol] = std::pow(source[source_index].r, 2) + std::pow(source[source_index].i, 2);
subcarrier++;
}
if (wrapped) {
for (auto sc = wrapped_first_sc; sc <= wrapped_last_sc; sc++) {
auto source_index = sc + symbol * ofdm_symbol_size;
power[subcarrier * num_symbols + symbol] = std::pow(source[source_index].r, 2) + std::pow(source[source_index].i, 2);
subcarrier++;
}
}
}
max = *std::max_element(power.begin(), power.end());
if (frozen && max > stop_at_min) {
next = false;
}
scope_data->is_data_ready = false;
}
}
}
void Draw(float time, int ofdm_symbol_size, int num_symbols, int first_carrier_offset, int num_rb)
{
ReadData(time, ofdm_symbol_size, num_symbols, first_carrier_offset, num_rb);
ImGui::BeginGroup();
if (ImGui::Button(frozen ? "Unfreeze" : "Freeze")) {
frozen = !frozen;
next = false;
}
if (frozen) {
ImGui::SameLine();
ImGui::BeginDisabled(next);
if (ImGui::Button("Load next data")) {
next = true;
}
ImGui::EndDisabled();
ImGui::Text("Filter parameters:");
ImGui::InputFloat("Max Power minimum", &stop_at_min, 10, 100);
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip("Data with maximum power below that value will be discarded.");
}
}
static std::vector<int> symbol_boundaries = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13};
if (ImPlot::BeginPlot(label.c_str(), {(float)ImGui::GetWindowWidth() * 0.9f, 0})) {
auto num_sc = num_rb * NR_NB_SC_PER_RB;
ImPlot::SetupAxes("symbols", "subcarriers");
ImPlot::SetupAxisLimits(ImAxis_X1, num_symbols, 0);
ImPlot::SetupAxisLimits(ImAxis_Y1, num_sc, 0);
ImPlot::PlotHeatmap(label.c_str(),
power.data(),
num_sc,
num_symbols,
0,
max,
nullptr,
{0, 0},
{(double)num_symbols, (double)num_sc});
ImPlot::PlotInfLines("Symbol boundary", symbol_boundaries.data(), symbol_boundaries.size());
ImPlot::PlotInfLines("RB boundary", rb_boundaries.data(), num_rb, ImPlotInfLinesFlags_Horizontal);
ImPlot::EndPlot();
}
ImGui::SameLine();
ImPlot::ColormapScale("##HeatScale", 0, max);
ImGui::Text("Data is %.2f seconds old", time - timestamp);
ImGui::EndGroup();
}
};
// utility structure for realtime plot
struct ScrollingBuffer {
int MaxSize;
int Offset;
ImVector<ImVec2> Data;
ScrollingBuffer(int max_size = 2000)
{
MaxSize = max_size;
Offset = 0;
Data.reserve(MaxSize);
}
void AddPoint(float x, float y)
{
if (Data.size() < MaxSize)
Data.push_back(ImVec2(x, y));
else {
Data[Offset] = ImVec2(x, y);
Offset = (Offset + 1) % MaxSize;
}
}
void Erase()
{
if (Data.size() > 0) {
Data.shrink(0);
Offset = 0;
}
}
};
void ShowUeScope(PHY_VARS_NR_UE *ue, float t)
{
ImGui::Begin("UE KPI");
if (ImPlot::BeginPlot("##Scrolling", ImVec2(-1, 150))) {
static float history = 10.0f;
ImGui::SliderFloat("History", &history, 1, 30, "%.1f s");
static ScrollingBuffer rbs_buffer;
static ScrollingBuffer bler;
static ScrollingBuffer mcs;
rbs_buffer.AddPoint(t, getKPIUE()->nofRBs);
bler.AddPoint(t, (float)getKPIUE()->nb_nack / (float)getKPIUE()->nb_total);
mcs.AddPoint(t, (float)getKPIUE()->dl_mcs);
ImPlot::SetupAxes("time", "noOfRbs");
ImPlot::SetupAxisLimits(ImAxis_X1, t - history, t, ImGuiCond_Always);
ImPlot::SetupAxisLimits(ImAxis_Y1, 0, NR_MAX_RB);
ImPlot::SetupAxis(ImAxis_Y2, "bler [%]", ImPlotAxisFlags_AuxDefault);
ImPlot::SetupAxis(ImAxis_Y3, "MCS", ImPlotAxisFlags_AuxDefault);
ImPlot::SetAxes(ImAxis_X1, ImAxis_Y1);
ImPlot::PlotLine("noOfRbs", &rbs_buffer.Data[0].x, &rbs_buffer.Data[0].y, rbs_buffer.Data.size(), 0, 0, 2 * sizeof(float));
ImPlot::SetAxes(ImAxis_X1, ImAxis_Y2);
ImPlot::PlotLine("bler", &bler.Data[0].x, &bler.Data[0].y, bler.Data.size(), 0, 0, 2 * sizeof(float));
ImPlot::SetAxes(ImAxis_X1, ImAxis_Y3);
ImPlot::PlotLine("mcs", &mcs.Data[0].x, &mcs.Data[0].y, mcs.Data.size(), 0, 0, 2 * sizeof(float));
ImPlot::EndPlot();
}
ImGui::End();
if (ImGui::Begin("UE PDSCH IQ")) {
static auto iq_data = new IQData();
static auto pdsch_iq_hist = new IQHist("PDSCH IQ");
bool new_data = false;
if (pdsch_iq_hist->ShouldReadData()) {
new_data = iq_data->TryCollect(&scope_array[pdschRxdataF_comp], t, pdsch_iq_hist->GetEpsilon());
}
pdsch_iq_hist->Draw(iq_data, t, new_data);
}
ImGui::End();
if (ImGui::Begin("Time domain samples")) {
static auto iq_data = new IQData();
// Issue with imgui deferring draw calls until the end of the frame - cases segfault if scatterplot has too many points
bool disable_scatterplot = true;
static auto time_domain_iq = new IQHist("Time domain samples", disable_scatterplot);
bool new_data = false;
if (time_domain_iq->ShouldReadData()) {
new_data = iq_data->TryCollect(&scope_array[ueTimeDomainSamples], t, time_domain_iq->GetEpsilon());
}
time_domain_iq->Draw(iq_data, t, new_data);
}
ImGui::End();
if (ImGui::Begin("Time domain samples - before sync")) {
static auto iq_data = new IQData();
// Issue with imgui deferring draw calls until the end of the frame - cases segfault if scatterplot has too many points
bool disable_scatterplot = true;
static auto time_domain_iq = new IQHist("Time domain samples - before sync", disable_scatterplot);
bool new_data = false;
if (time_domain_iq->ShouldReadData()) {
new_data = iq_data->TryCollect(&scope_array[ueTimeDomainSamplesBeforeSync], t, time_domain_iq->GetEpsilon());
}
time_domain_iq->Draw(iq_data, t, new_data);
}
ImGui::End();
if (ImGui::Begin("Broadcast channel")) {
ImGui::Text("RSRP %d", ue->measurements.ssb_rsrp_dBm[ue->frame_parms.ssb_index]);
if (ImGui::TreeNode("IQ")) {
static auto iq_data = new IQData();
static auto broadcast_iq_hist = new IQHist("Broadcast IQ");
bool new_data = false;
if (broadcast_iq_hist->ShouldReadData()) {
new_data = iq_data->TryCollect(&scope_array[ue->sl_mode ? psbchRxdataF_comp : pbchRxdataF_comp],
t,
broadcast_iq_hist->GetEpsilon());
}
broadcast_iq_hist->Draw(iq_data, t, new_data);
ImGui::TreePop();
}
if (ImGui::TreeNode("CHest")) {
static auto chest_iq_data = new IQData();
static auto broadcast_iq_chest = new IQHist("Broadcast Chest");
bool new_data = false;
if (broadcast_iq_chest->ShouldReadData()) {
new_data = chest_iq_data->TryCollect(&scope_array[ue->sl_mode ? psbchDlChEstimateTime : pbchDlChEstimateTime],
t,
broadcast_iq_chest->GetEpsilon());
}
broadcast_iq_chest->Draw(chest_iq_data, t, new_data);
ImGui::TreePop();
}
if (ImGui::TreeNode("LLR")) {
static auto llr_plot = new LLRPlot();
llr_plot->Draw(t, ue->sl_mode ? psbchLlr : pbchLlr, "Broadcast LLR");
ImGui::TreePop();
}
}
ImGui::End();
// if (ImGui::Begin("RX IQ")) {
// static auto common_rx_iq_heatmap = new IQSlotHeatmap(&scope_array[commonRxdataF], "common RX IQ");
// common_rx_iq_heatmap->Draw(t,
// ue->frame_parms.ofdm_symbol_size,
// ue->frame_parms.symbols_per_slot,
// ue->frame_parms.first_carrier_offset,
// ue->frame_parms.N_RB_DL);
// }
// ImGui::End();
}
void ShowGnbScope(PHY_VARS_gNB *gNB, float t)
{
// if (ImGui::TreeNode("RX IQ")) {
// static auto gnb_heatmap = new IQSlotHeatmap(&scope_array[gNBRxdataF], "common RX IQ");
// gnb_heatmap->Draw(t,
// gNB->frame_parms.ofdm_symbol_size,
// gNB->frame_parms.symbols_per_slot,
// gNB->frame_parms.first_carrier_offset,
// gNB->frame_parms.N_RB_UL);
// ImGui::TreePop();
// }
if (ImGui::Begin("PUSCH SLOT IQ")) {
static auto pusch_iq = new IQData();
static auto pusch_iq_display = new IQHist("PUSCH compensated IQ");
bool new_data = false;
if (pusch_iq_display->ShouldReadData()) {
new_data = pusch_iq->TryCollect(&scope_array[gNBPuschRxIq], t, pusch_iq_display->GetEpsilon());
}
pusch_iq_display->Draw(pusch_iq, t, new_data);
}
ImGui::End();
if (ImGui::Begin("PUSCH LLRs")) {
static auto pusch_llr_plot = new LLRPlot();
pusch_llr_plot->Draw(t, gNBPuschLlr, "PUSCH LLR");
}
ImGui::End();
if (ImGui::Begin("Time domain samples")) {
static auto iq_data = new IQData();
// Issue with imgui deferring draw calls until the end of the frame - cases segfault if scatterplot has too many points
bool disable_scatterplot = true;
static auto time_domain_iq = new IQHist("Time domain samples", disable_scatterplot);
bool new_data = false;
if (time_domain_iq->ShouldReadData()) {
new_data = iq_data->TryCollect(&scope_array[gNbTimeDomainSamples], t, time_domain_iq->GetEpsilon());
}
time_domain_iq->Draw(iq_data, t, new_data);
}
ImGui::End();
}
void *imscope_thread(void *data_void_ptr)
{
glfwSetErrorCallback(glfw_error_callback);
if (!glfwInit())
return nullptr;
// Decide GL+GLSL versions
#if defined(IMGUI_IMPL_OPENGL_ES2)
// GL ES 2.0 + GLSL 100
const char *glsl_version = "#version 100";
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 2);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 0);
glfwWindowHint(GLFW_CLIENT_API, GLFW_OPENGL_ES_API);
#elif defined(__APPLE__)
// GL 3.2 + GLSL 150
const char *glsl_version = "#version 150";
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 2);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); // 3.2+ only
glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE); // Required on Mac
#else
// GL 3.0 + GLSL 130
const char *glsl_version = "#version 130";
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 0);
// glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); // 3.2+ only
// glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE); // 3.0+ only
#endif
// Create window with graphics context
GLFWwindow *window = glfwCreateWindow(1280, 720, "imscope", nullptr, nullptr);
if (window == nullptr)
return nullptr;
glfwMakeContextCurrent(window);
glfwSwapInterval(1); // For frame capping
// Setup Dear ImGui context
IMGUI_CHECKVERSION();
ImGui::CreateContext();
ImPlot::CreateContext();
ImGuiIO &io = ImGui::GetIO();
(void)io;
io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard; // Enable Keyboard Controls
io.ConfigFlags |= ImGuiConfigFlags_DockingEnable;
// Setup Dear ImGui style
ImGui::StyleColorsDark();
// ImGui::StyleColorsLight();
// Setup Platform/Renderer backends
ImGui_ImplGlfw_InitForOpenGL(window, true);
#ifdef __EMSCRIPTEN__
ImGui_ImplGlfw_InstallEmscriptenCallbacks(window, "#canvas");
#endif
ImGui_ImplOpenGL3_Init(glsl_version);
// Our state
ImVec4 clear_color = ImVec4(0.45f, 0.55f, 0.60f, 1.00f);
for (auto i = 0U; i < NR_MAX_RB; i++) {
rb_boundaries.push_back(i * NR_NB_SC_PER_RB);
}
static double last_frame_time = glfwGetTime();
static int target_fps = 24;
bool is_ue = IS_SOFTMODEM_5GUE;
bool close_window = false;
while (!glfwWindowShouldClose(window) && close_window == false) {
// Poll and handle events (inputs, window resize, etc.)
// You can read the io.WantCaptureMouse, io.WantCaptureKeyboard flags to tell if dear imgui wants to use your inputs.
// - When io.WantCaptureMouse is true, do not dispatch mouse input data to your main application, or clear/overwrite your copy
// of the mouse data.
// - When io.WantCaptureKeyboard is true, do not dispatch keyboard input data to your main application, or clear/overwrite your
// copy of the keyboard data. Generally you may always pass all inputs to dear imgui, and hide them from your application based
// on those two flags.
glfwPollEvents();
// Start the Dear ImGui frame
ImGui_ImplOpenGL3_NewFrame();
ImGui_ImplGlfw_NewFrame();
static bool reset_ini_settings = false;
if (reset_ini_settings)
{
ImGui::LoadIniSettingsFromDisk("imscope-init.ini");
reset_ini_settings = false;
}
ImGui::NewFrame();
int display_w, display_h;
glfwGetFramebufferSize(window, &display_w, &display_h);
static float t = 0;
static bool show_imgui_demo_window = false;
static bool show_implot_demo_window = false;
ImGui::DockSpaceOverViewport();
if (ImGui::BeginMainMenuBar()) {
if (ImGui::BeginMenu("File")) {
if (ImGui::MenuItem("Close scope")) {
close_window = true;
}
ImGui::EndMenu();
}
if (ImGui::BeginMenu("Options")) {
ImGui::Checkbox("Show imgui demo window", &show_imgui_demo_window);
ImGui::Checkbox("Show implot demo window", &show_implot_demo_window);
ImGui::EndMenu();
}
if (ImGui::BeginMenu("Layout")) {
if (ImGui::MenuItem("Reset")) {
reset_ini_settings = true;
}
ImGui::EndMenu();
}
ImGui::EndMainMenuBar();
}
ImGui::Begin("Status bar");
ImGui::Text("Total time used by IQ capture procedures per milisecond: %.2f [us]/[ms]", iq_procedure_timer.average / 1000);
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip("Total time used in PHY threads for copying out IQ data for the scope, in uS, averaged over 1 ms");
}
ImGui::End();
ImGui::Begin("Global scope settings");
ImGui::ShowStyleSelector("ImGui Style");
ImPlot::ShowStyleSelector("ImPlot Style");
ImPlot::ShowColormapSelector("ImPlot Colormap");
ImGui::SliderInt("FPS target", &target_fps, 12, 60);
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip("Reduces scope flickering in unfrozen mode. Can reduce impact on perfromance of the modem");
}
ImGui::End();
t += ImGui::GetIO().DeltaTime;
iq_procedure_timer.UpdateAverage(t);
if (is_ue) {
PHY_VARS_NR_UE *ue = (PHY_VARS_NR_UE *)data_void_ptr;
ShowUeScope(ue, t);
} else {
scopeParms_t *scope_params = (scopeParms_t *)data_void_ptr;
PHY_VARS_gNB *gNB = scope_params->gNB;
ShowGnbScope(gNB, t);
}
// For reference
if (show_implot_demo_window)
ImPlot::ShowDemoWindow();
if (show_imgui_demo_window)
ImGui::ShowDemoWindow();
// Rendering
ImGui::Render();
glViewport(0, 0, display_w, display_h);
glClearColor(clear_color.x * clear_color.w, clear_color.y * clear_color.w, clear_color.z * clear_color.w, clear_color.w);
glClear(GL_COLOR_BUFFER_BIT);
ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());
double target_frame_time = 1.0 / target_fps;
double delta = glfwGetTime() - last_frame_time;
if (delta < target_frame_time) {
std::this_thread::sleep_for(std::chrono::duration<float>(target_frame_time - delta));
}
glfwSwapBuffers(window);
last_frame_time = glfwGetTime();
}
// Cleanup
ImGui_ImplOpenGL3_Shutdown();
ImGui_ImplGlfw_Shutdown();
ImGui::DestroyContext();
glfwDestroyWindow(window);
glfwTerminate();
return nullptr;
}
extern "C" void imscope_autoinit(void *dataptr)
{
AssertFatal(IS_SOFTMODEM_5GUE || IS_SOFTMODEM_GNB, "Scope cannot find NRUE or GNB context");
for (auto i = 0U; i < EXTRA_SCOPE_TYPES; i++) {
scope_array[i].is_data_ready = false;
scope_array[i].scope_graph_data = nullptr;
scope_array[i].meta = {-1, -1};
}
if (IS_SOFTMODEM_GNB) {
scopeParms_t *scope_params = (scopeParms_t *)dataptr;
scopeData_t *scope = (scopeData_t *)calloc(1, sizeof(scopeData_t));
scope->copyData = copyDataThreadSafe;
scope->tryLockScopeData = tryLockScopeData;
scope->copyDataUnsafeWithOffset = copyDataUnsafeWithOffset;
scope->unlockScopeData = unlockScopeData;
scope_params->gNB->scopeData = scope;
scope_params->ru->scopeData = scope;
} else {
PHY_VARS_NR_UE *ue = (PHY_VARS_NR_UE *)dataptr;
scopeData_t *scope = (scopeData_t *)calloc(1, sizeof(scopeData_t));
scope->copyData = copyDataThreadSafe;
ue->scopeData = scope;
}
pthread_t thread;
threadCreate(&thread, imscope_thread, dataptr, (char *)"imscope", -1, sched_get_priority_min(SCHED_RR));
}
void copyDataThreadSafe(void *scopeData,
enum scopeDataType type,
void *dataIn,
int elementSz,
int colSz,
int lineSz,
int offset,
metadata *meta)
{
ImScopeData &scope_data = scope_array[type];
if (scope_data.is_data_ready) {
// data is ready, wasn't consumed yet by scope
return;
}
if (scope_data.write_mutex.try_lock()) {
auto start = std::chrono::high_resolution_clock::now();
scopeGraphData_t *data = scope_data.scope_graph_data;
int oldDataSz = data ? data->dataSize : 0;
int newSz = elementSz * colSz * lineSz;
if (data == NULL || oldDataSz < newSz) {
free(data);
scopeGraphData_t *ptr = (scopeGraphData_t *)malloc(sizeof(scopeGraphData_t) + newSz);
if (!ptr) {
LOG_E(PHY, "can't realloc\n");
return;
} else {
data = ptr;
}
}
data->elementSz = elementSz;
data->colSz = colSz;
data->lineSz = lineSz;
data->dataSize = newSz;
memcpy(((void *)(data + 1)), dataIn, newSz);
scope_data.scope_graph_data = data;
scope_data.meta = *meta;
scope_data.time_taken_in_ns =
std::chrono::duration_cast<std::chrono::nanoseconds>(std::chrono::high_resolution_clock::now() - start).count();
scope_data.is_data_ready = true;
scope_data.write_mutex.unlock();
}
}
bool tryLockScopeData(enum scopeDataType type, int elementSz, int colSz, int lineSz, metadata *meta)
{
ImScopeData &scope_data = scope_array[type];
if (scope_data.is_data_ready) {
// data is ready, wasn't consumed yet by scope
return false;
}
if (scope_data.write_mutex.try_lock()) {
auto start = std::chrono::high_resolution_clock::now();
scopeGraphData_t *data = scope_data.scope_graph_data;
int oldDataSz = data ? data->dataSize : 0;
int newSz = elementSz * colSz * lineSz;
if (data == NULL || oldDataSz < newSz) {
free(data);
scopeGraphData_t *ptr = (scopeGraphData_t *)malloc(sizeof(scopeGraphData_t) + newSz);
if (!ptr) {
LOG_E(PHY, "can't realloc\n");
return false;
} else {
data = ptr;
}
}
data->elementSz = elementSz;
data->colSz = colSz;
data->lineSz = lineSz;
data->dataSize = newSz;
scope_data.scope_graph_data = data;
scope_data.meta = *meta;
scope_data.time_taken_in_ns =
std::chrono::duration_cast<std::chrono::nanoseconds>(std::chrono::high_resolution_clock::now() - start).count();
memset(scope_data.time_taken_in_ns_per_offset, 0, sizeof(scope_data.time_taken_in_ns_per_offset));
memset(scope_data.data_copied_per_offset, 0, sizeof(scope_data.data_copied_per_offset));
return true;
}
return false;
}
void copyDataUnsafeWithOffset(enum scopeDataType type, void *dataIn, size_t size, size_t offset, int copy_index)
{
AssertFatal(copy_index < MAX_OFFSETS, "Unexpected number of copies per sink. copy_index = %d\n", copy_index);
ImScopeData &scope_data = scope_array[type];
auto start = std::chrono::high_resolution_clock::now();
scopeGraphData_t *data = scope_data.scope_graph_data;
uint8_t *outptr = (uint8_t *)(data + 1);
memcpy(&outptr[offset], dataIn, size);
scope_data.time_taken_in_ns_per_offset[copy_index] =
std::chrono::duration_cast<std::chrono::nanoseconds>(std::chrono::high_resolution_clock::now() - start).count();
scope_data.data_copied_per_offset[copy_index] = size;
}
void unlockScopeData(enum scopeDataType type)
{
ImScopeData &scope_data = scope_array[type];
size_t total_size = 0;
for (auto i = 0; i < MAX_OFFSETS; i++) {
scope_data.time_taken_in_ns += scope_data.time_taken_in_ns_per_offset[i];
total_size += scope_data.data_copied_per_offset[i];
}
if (total_size != (uint64_t)scope_data.scope_graph_data->dataSize) {
LOG_E(PHY,
"Scope is missing data - not all data that was expected was copied - possibly missed copyDataUnsafeWithOffset call\n");
}
scope_data.is_data_ready = true;
scope_data.write_mutex.unlock();
}