mirror of
https://github.com/torlando-tech/pyxis.git
synced 2026-08-25 20:19:52 +00:00
352 lines
13 KiB
C++
352 lines
13 KiB
C++
#include "RadioActivityScreen.h"
|
|
|
|
#ifdef ARDUINO
|
|
|
|
#include "Theme.h"
|
|
#include "../LVGL/LVGLInit.h"
|
|
#include "../LVGL/LVGLLock.h"
|
|
|
|
namespace UI {
|
|
namespace LXMF {
|
|
namespace {
|
|
|
|
constexpr uint32_t RENDER_INTERVAL_MS = 143; // Seven frames per second.
|
|
constexpr int16_t GRAPH_MIN_DBM = -135;
|
|
constexpr int16_t GRAPH_MAX_DBM = -60;
|
|
|
|
void style_panel(lv_obj_t* panel) {
|
|
lv_obj_set_style_bg_color(panel, Theme::surfaceInput(), 0);
|
|
lv_obj_set_style_bg_opa(panel, LV_OPA_COVER, 0);
|
|
lv_obj_set_style_border_color(panel, Theme::border(), 0);
|
|
lv_obj_set_style_border_width(panel, 1, 0);
|
|
lv_obj_set_style_radius(panel, 4, 0);
|
|
lv_obj_set_style_pad_all(panel, 3, 0);
|
|
}
|
|
|
|
lv_obj_t* metric_panel(lv_obj_t* parent, int x, int width,
|
|
const char* title, lv_obj_t** value) {
|
|
lv_obj_t* panel = lv_obj_create(parent);
|
|
lv_obj_set_pos(panel, x, 40);
|
|
lv_obj_set_size(panel, width, 34);
|
|
lv_obj_clear_flag(panel, LV_OBJ_FLAG_SCROLLABLE);
|
|
style_panel(panel);
|
|
|
|
lv_obj_t* heading = lv_label_create(panel);
|
|
lv_label_set_text(heading, title);
|
|
lv_obj_align(heading, LV_ALIGN_TOP_LEFT, 0, -1);
|
|
lv_obj_set_style_text_font(heading, &lv_font_montserrat_8, 0);
|
|
lv_obj_set_style_text_color(heading, Theme::textMuted(), 0);
|
|
|
|
*value = lv_label_create(panel);
|
|
lv_label_set_text(*value, "--");
|
|
lv_obj_align(*value, LV_ALIGN_BOTTOM_LEFT, 0, 1);
|
|
lv_obj_set_style_text_font(*value, &lv_font_montserrat_12, 0);
|
|
lv_obj_set_style_text_color(*value, Theme::textPrimary(), 0);
|
|
return panel;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
RadioActivityScreen::RadioActivityScreen(lv_obj_t* parent) {
|
|
_screen = lv_obj_create(parent ? parent : lv_scr_act());
|
|
lv_obj_set_size(_screen, LV_PCT(100), LV_PCT(100));
|
|
lv_obj_clear_flag(_screen, LV_OBJ_FLAG_SCROLLABLE);
|
|
lv_obj_set_style_bg_color(_screen, Theme::surface(), 0);
|
|
lv_obj_set_style_bg_opa(_screen, LV_OPA_COVER, 0);
|
|
lv_obj_set_style_border_width(_screen, 0, 0);
|
|
lv_obj_set_style_radius(_screen, 0, 0);
|
|
lv_obj_set_style_pad_all(_screen, 0, 0);
|
|
|
|
create_header();
|
|
create_metrics();
|
|
create_graph();
|
|
create_footer();
|
|
hide();
|
|
}
|
|
|
|
RadioActivityScreen::~RadioActivityScreen() {
|
|
LVGL_LOCK();
|
|
if (_screen) lv_obj_del(_screen);
|
|
}
|
|
|
|
void RadioActivityScreen::create_header() {
|
|
lv_obj_t* header = lv_obj_create(_screen);
|
|
lv_obj_set_pos(header, 0, 0);
|
|
lv_obj_set_size(header, 320, 36);
|
|
lv_obj_clear_flag(header, LV_OBJ_FLAG_SCROLLABLE);
|
|
lv_obj_set_style_bg_color(header, Theme::surfaceHeader(), 0);
|
|
lv_obj_set_style_border_width(header, 0, 0);
|
|
lv_obj_set_style_radius(header, 0, 0);
|
|
lv_obj_set_style_pad_all(header, 0, 0);
|
|
|
|
_btn_back = lv_btn_create(header);
|
|
lv_obj_set_size(_btn_back, 44, 28);
|
|
lv_obj_align(_btn_back, LV_ALIGN_LEFT_MID, 7, 0);
|
|
lv_obj_set_style_bg_color(_btn_back, Theme::btnSecondary(), 0);
|
|
lv_obj_set_style_border_color(_btn_back, Theme::border(), 0);
|
|
lv_obj_set_style_border_width(_btn_back, 1, 0);
|
|
lv_obj_add_event_cb(_btn_back, on_back_clicked, LV_EVENT_CLICKED, this);
|
|
lv_obj_t* back = lv_label_create(_btn_back);
|
|
lv_label_set_text(back, LV_SYMBOL_LEFT);
|
|
lv_obj_center(back);
|
|
|
|
lv_obj_t* title = lv_label_create(header);
|
|
lv_label_set_text(title, "Radio Activity");
|
|
lv_obj_align(title, LV_ALIGN_LEFT_MID, 60, 0);
|
|
lv_obj_set_style_text_font(title, &lv_font_montserrat_14, 0);
|
|
lv_obj_set_style_text_color(title, Theme::textPrimary(), 0);
|
|
|
|
lv_obj_t* live = lv_label_create(header);
|
|
lv_label_set_text(live, LV_SYMBOL_BULLET " LIVE | 7 FPS");
|
|
lv_obj_align(live, LV_ALIGN_RIGHT_MID, -7, 0);
|
|
lv_obj_set_style_text_font(live, &lv_font_montserrat_8, 0);
|
|
lv_obj_set_style_text_color(live, Theme::success(), 0);
|
|
}
|
|
|
|
void RadioActivityScreen::create_metrics() {
|
|
metric_panel(_screen, 7, 100, "CURRENT", &_label_current);
|
|
metric_panel(_screen, 111, 100, "NOISE FLOOR", &_label_noise);
|
|
metric_panel(_screen, 215, 98, "CHANNEL LOAD", &_label_load);
|
|
lv_obj_set_style_text_color(_label_load, Theme::success(), 0);
|
|
}
|
|
|
|
void RadioActivityScreen::create_graph() {
|
|
lv_obj_t* graph_panel = lv_obj_create(_screen);
|
|
lv_obj_set_pos(graph_panel, 7, 78);
|
|
lv_obj_set_size(graph_panel, 306, 116);
|
|
lv_obj_clear_flag(graph_panel, LV_OBJ_FLAG_SCROLLABLE);
|
|
style_panel(graph_panel);
|
|
lv_obj_set_style_pad_all(graph_panel, 0, 0);
|
|
|
|
const char* scales[] = {"-60", "-100", "-135"};
|
|
const int ys[] = {3, 49, 98};
|
|
for (int i = 0; i < 3; ++i) {
|
|
lv_obj_t* label = lv_label_create(graph_panel);
|
|
lv_label_set_text(label, scales[i]);
|
|
lv_obj_set_pos(label, 3, ys[i]);
|
|
lv_obj_set_style_text_font(label, &lv_font_montserrat_8, 0);
|
|
lv_obj_set_style_text_color(label, Theme::textMuted(), 0);
|
|
}
|
|
|
|
// A plain draw object avoids both a full-screen canvas and LVGL's disabled
|
|
// chart widget. The callback emits only bounded line primitives.
|
|
_chart = lv_obj_create(graph_panel);
|
|
lv_obj_set_pos(_chart, 27, 3);
|
|
lv_obj_set_size(_chart, 276, 108);
|
|
lv_obj_clear_flag(_chart, LV_OBJ_FLAG_SCROLLABLE);
|
|
lv_obj_set_style_bg_color(_chart, Theme::surface(), 0);
|
|
lv_obj_set_style_bg_opa(_chart, LV_OPA_COVER, 0);
|
|
lv_obj_set_style_border_width(_chart, 0, 0);
|
|
lv_obj_set_style_radius(_chart, 0, 0);
|
|
lv_obj_set_style_pad_all(_chart, 0, 0);
|
|
lv_obj_add_event_cb(_chart, on_chart_draw, LV_EVENT_DRAW_POST_END, this);
|
|
}
|
|
|
|
void RadioActivityScreen::create_footer() {
|
|
auto style_legend = [](lv_obj_t* label, int x, lv_color_t color) {
|
|
lv_obj_set_pos(label, x, 198);
|
|
lv_obj_set_style_text_font(label, &lv_font_montserrat_8, 0);
|
|
lv_obj_set_style_text_color(label, color, 0);
|
|
};
|
|
|
|
lv_obj_t* legend_noise = lv_label_create(_screen);
|
|
lv_label_set_text(legend_noise, LV_SYMBOL_BULLET " Noise");
|
|
style_legend(legend_noise, 9, lv_color_hex(0x675A70));
|
|
|
|
lv_obj_t* legend_rx = lv_label_create(_screen);
|
|
lv_label_set_text(legend_rx, LV_SYMBOL_BULLET " LoRa RX");
|
|
style_legend(legend_rx, 72, lv_color_hex(0x55966B));
|
|
|
|
lv_obj_t* legend_other = lv_label_create(_screen);
|
|
lv_label_set_text(legend_other, LV_SYMBOL_BULLET " Other RF");
|
|
style_legend(legend_other, 155, lv_color_hex(0xA98BAE));
|
|
|
|
lv_obj_t* legend_tx = lv_label_create(_screen);
|
|
lv_label_set_text(legend_tx, LV_SYMBOL_BULLET " TX");
|
|
style_legend(legend_tx, 264, Theme::primaryLight());
|
|
|
|
_label_footer = lv_label_create(_screen);
|
|
lv_label_set_text(_label_footer, "Radio unavailable");
|
|
lv_obj_set_pos(_label_footer, 0, 220);
|
|
lv_obj_set_width(_label_footer, 320);
|
|
lv_obj_set_style_text_align(_label_footer, LV_TEXT_ALIGN_CENTER, 0);
|
|
lv_obj_set_style_text_font(_label_footer, &lv_font_montserrat_8, 0);
|
|
lv_obj_set_style_text_color(_label_footer, Theme::textTertiary(), 0);
|
|
}
|
|
|
|
void RadioActivityScreen::render(const RadioActivity::Snapshot& snapshot,
|
|
const RadioConfig& config,
|
|
uint32_t now_ms) {
|
|
if (!render_due(now_ms)) return;
|
|
_last_render_ms = now_ms;
|
|
|
|
LVGL_LOCK();
|
|
_draw_snapshot = snapshot;
|
|
|
|
char text[64];
|
|
if (snapshot.current_rssi_valid) {
|
|
snprintf(text, sizeof(text), "%d dBm", snapshot.current_rssi);
|
|
} else {
|
|
snprintf(text, sizeof(text), "-- dBm");
|
|
}
|
|
lv_label_set_text(_label_current, text);
|
|
|
|
if (snapshot.noise_floor_ready) {
|
|
snprintf(text, sizeof(text), "%d dBm", snapshot.noise_floor);
|
|
} else {
|
|
snprintf(text, sizeof(text), "Learning...");
|
|
}
|
|
lv_label_set_text(_label_noise, text);
|
|
snprintf(text, sizeof(text), "%u%%", snapshot.channel_load_percent);
|
|
lv_label_set_text(_label_load, text);
|
|
|
|
if (config.available) {
|
|
snprintf(text, sizeof(text), "%.3f MHz | BW %.1f kHz | SF%u | CR4/%u | %d dBm",
|
|
config.frequency_mhz, config.bandwidth_khz,
|
|
config.spreading_factor, config.coding_rate, config.tx_power_dbm);
|
|
} else {
|
|
snprintf(text, sizeof(text), "Radio unavailable");
|
|
}
|
|
lv_label_set_text(_label_footer, text);
|
|
lv_obj_invalidate(_chart);
|
|
}
|
|
|
|
bool RadioActivityScreen::render_due(uint32_t now_ms) const {
|
|
return _visible && now_ms - _last_render_ms >= RENDER_INTERVAL_MS;
|
|
}
|
|
|
|
void RadioActivityScreen::show() {
|
|
LVGL_LOCK();
|
|
_visible = true;
|
|
_last_render_ms = millis() - RENDER_INTERVAL_MS;
|
|
lv_obj_clear_flag(_screen, LV_OBJ_FLAG_HIDDEN);
|
|
lv_obj_move_foreground(_screen);
|
|
lv_group_t* group = LVGL::LVGLInit::get_default_group();
|
|
if (group && _btn_back) {
|
|
lv_group_add_obj(group, _btn_back);
|
|
lv_group_focus_obj(_btn_back);
|
|
}
|
|
}
|
|
|
|
void RadioActivityScreen::hide() {
|
|
LVGL_LOCK();
|
|
_visible = false;
|
|
lv_group_t* group = LVGL::LVGLInit::get_default_group();
|
|
if (group && _btn_back) lv_group_remove_obj(_btn_back);
|
|
lv_obj_add_flag(_screen, LV_OBJ_FLAG_HIDDEN);
|
|
}
|
|
|
|
void RadioActivityScreen::set_back_callback(BackCallback callback) {
|
|
_back_callback = callback;
|
|
}
|
|
|
|
void RadioActivityScreen::on_back_clicked(lv_event_t* event) {
|
|
auto* screen = static_cast<RadioActivityScreen*>(lv_event_get_user_data(event));
|
|
if (screen && screen->_back_callback) screen->_back_callback();
|
|
}
|
|
|
|
void RadioActivityScreen::on_chart_draw(lv_event_t* event) {
|
|
auto* screen = static_cast<RadioActivityScreen*>(lv_event_get_user_data(event));
|
|
if (!screen || screen->_draw_snapshot.count == 0) return;
|
|
|
|
lv_draw_ctx_t* draw_ctx = lv_event_get_draw_ctx(event);
|
|
lv_area_t area;
|
|
lv_obj_get_content_coords(screen->_chart, &area);
|
|
const int32_t width = lv_area_get_width(&area) - 1;
|
|
const int32_t height = lv_area_get_height(&area) - 1;
|
|
const auto& snapshot = screen->_draw_snapshot;
|
|
|
|
auto x_for = [&](std::size_t index) -> lv_coord_t {
|
|
const std::size_t right_aligned = RadioActivity::History::CAPACITY - snapshot.count + index;
|
|
return static_cast<lv_coord_t>(area.x1 + static_cast<int32_t>(
|
|
right_aligned * width / (RadioActivity::History::CAPACITY - 1)));
|
|
};
|
|
auto y_for = [&](int16_t rssi) -> lv_coord_t {
|
|
if (rssi < GRAPH_MIN_DBM) rssi = GRAPH_MIN_DBM;
|
|
if (rssi > GRAPH_MAX_DBM) rssi = GRAPH_MAX_DBM;
|
|
return static_cast<lv_coord_t>(area.y1 + static_cast<int32_t>(
|
|
(GRAPH_MAX_DBM - rssi) * height / (GRAPH_MAX_DBM - GRAPH_MIN_DBM)));
|
|
};
|
|
|
|
lv_draw_line_dsc_t grid_dsc;
|
|
lv_draw_line_dsc_init(&grid_dsc);
|
|
grid_dsc.color = Theme::border();
|
|
grid_dsc.width = 1;
|
|
grid_dsc.opa = LV_OPA_30;
|
|
for (const int16_t dbm : {static_cast<int16_t>(-60), static_cast<int16_t>(-100),
|
|
static_cast<int16_t>(-135)}) {
|
|
lv_point_t p1 = {area.x1, y_for(dbm)};
|
|
lv_point_t p2 = {area.x2, y_for(dbm)};
|
|
lv_draw_line(draw_ctx, &grid_dsc, &p1, &p2);
|
|
}
|
|
for (int division = 1; division < 4; ++division) {
|
|
const lv_coord_t x = static_cast<lv_coord_t>(area.x1 + width * division / 4);
|
|
lv_point_t p1 = {x, area.y1};
|
|
lv_point_t p2 = {x, area.y2};
|
|
lv_draw_line(draw_ctx, &grid_dsc, &p1, &p2);
|
|
}
|
|
|
|
if (snapshot.noise_floor_ready) {
|
|
lv_draw_line_dsc_t noise_dsc;
|
|
lv_draw_line_dsc_init(&noise_dsc);
|
|
noise_dsc.color = lv_color_hex(0xA98BAE);
|
|
noise_dsc.width = 1;
|
|
noise_dsc.opa = LV_OPA_60;
|
|
const lv_coord_t y = y_for(snapshot.noise_floor);
|
|
lv_point_t p1 = {area.x1, y};
|
|
lv_point_t p2 = {area.x2, y};
|
|
lv_draw_line(draw_ctx, &noise_dsc, &p1, &p2);
|
|
}
|
|
|
|
if (snapshot.count > 1) {
|
|
lv_draw_line_dsc_t trace_dsc;
|
|
lv_draw_line_dsc_init(&trace_dsc);
|
|
trace_dsc.color = lv_color_hex(0x675A70);
|
|
trace_dsc.width = 2;
|
|
trace_dsc.opa = LV_OPA_COVER;
|
|
for (std::size_t i = 1; i < snapshot.count; ++i) {
|
|
if (!snapshot.samples[i - 1].rssi_valid || !snapshot.samples[i].rssi_valid) {
|
|
continue;
|
|
}
|
|
lv_point_t p1 = {x_for(i - 1), y_for(snapshot.samples[i - 1].rssi_dbm)};
|
|
lv_point_t p2 = {x_for(i), y_for(snapshot.samples[i].rssi_dbm)};
|
|
lv_draw_line(draw_ctx, &trace_dsc, &p1, &p2);
|
|
}
|
|
}
|
|
|
|
for (std::size_t i = 0; i < snapshot.count; ++i) {
|
|
const auto& sample = snapshot.samples[i];
|
|
auto draw_marker = [&](RadioActivity::Event event_type, int32_t x_offset,
|
|
lv_color_t color, uint8_t line_width,
|
|
bool start_at_rssi) {
|
|
if (!RadioActivity::has_event(sample, event_type)) return;
|
|
int32_t marker_x = static_cast<int32_t>(x_for(i)) + x_offset;
|
|
if (marker_x < area.x1) marker_x = area.x1;
|
|
if (marker_x > area.x2) marker_x = area.x2;
|
|
const lv_coord_t y1 = start_at_rssi && sample.rssi_valid
|
|
? y_for(sample.rssi_dbm)
|
|
: area.y1;
|
|
|
|
lv_draw_line_dsc_t dsc;
|
|
lv_draw_line_dsc_init(&dsc);
|
|
dsc.color = color;
|
|
dsc.width = line_width;
|
|
dsc.opa = LV_OPA_80;
|
|
lv_point_t points[2] = {
|
|
{static_cast<lv_coord_t>(marker_x), y1},
|
|
{static_cast<lv_coord_t>(marker_x), area.y2},
|
|
};
|
|
lv_draw_line(draw_ctx, &dsc, &points[0], &points[1]);
|
|
};
|
|
|
|
draw_marker(RadioActivity::Event::Rx, -1, lv_color_hex(0x55966B), 2, true);
|
|
draw_marker(RadioActivity::Event::Interference, 0, lv_color_hex(0xA98BAE), 2, true);
|
|
draw_marker(RadioActivity::Event::Tx, 1, Theme::primaryLight(), 3, false);
|
|
}
|
|
}
|
|
|
|
} // namespace LXMF
|
|
} // namespace UI
|
|
|
|
#endif // ARDUINO
|