mirror of
https://github.com/MarekZegare4/MeshCore-Solo.git
synced 2026-11-01 02:11:57 +00:00
feat(ui-lvgl): charts with a value axis, grid and a finger's readout
Chart.h draws Diagnostics > History, the noise sweep and the trail's height
profile: round values up the left, labels along the bottom, a solid fill
(no transparency) and, under a finger, the sample's value and when / where.
Sim: sim_open("histdemo") / ("profdemo") fill them with made-up data.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,280 @@
|
||||
#pragma once
|
||||
// A chart drawn by its object (LV_EVENT_DRAW_MAIN_END): round values up the
|
||||
// left with a grid line each, labels along the bottom, the samples as a line
|
||||
// over a fill or as bars. A finger on it shows the sample under it -- a line
|
||||
// down the plot, the value and where / when on a pill. Diagnostics > History
|
||||
// and the trail's height profile (ProfileView.h) draw with it; the GPS bars
|
||||
// take the axis alone.
|
||||
//
|
||||
// No transparency: the fill is the accent mixed into the card's colour up
|
||||
// front, so the panel's renderer only ever copies solid colour.
|
||||
//
|
||||
// Single-TU fragment: included by ui-lvgl/UITask.cpp before ProfileView.h.
|
||||
|
||||
namespace chart {
|
||||
|
||||
static const int MAX = 128;
|
||||
|
||||
struct Chart {
|
||||
int16_t v[MAX];
|
||||
uint16_t x[MAX]; // with `spaced`: each sample's place, 0..10000 of the width
|
||||
int n = 0, cap = MAX; // without: newest at the right, `cap` samples wide
|
||||
bool spaced = false;
|
||||
bool bars = false;
|
||||
bool from_zero = false; // the scale starts at 0 (counts)
|
||||
int min_span = 1; // the scale spans at least this (a steady reading stays flat)
|
||||
int unit = 1; // the scale's steps are 1, 2, 5 x 10^k of this many units
|
||||
int top = 0; // room over the plot (a caption)
|
||||
uint32_t bg = theme::SURFACE; // what it's drawn on
|
||||
void (*axis)(int v, int step, char* out, size_t n) = nullptr; // a value up the left
|
||||
void (*value)(const Chart& c, int i, char* out, size_t n) = nullptr; // the pill: the sample...
|
||||
void (*where)(const Chart& c, int i, char* out, size_t n) = nullptr; // ...and where / when
|
||||
const char* under[5] = {}; // along the bottom, first at the left, last at the right
|
||||
int16_t mark = -1; // bars: one drawn in the accent, the rest in `bar`
|
||||
uint32_t bar = theme::ACCENT;
|
||||
int16_t touch = -1; // the sample under the finger
|
||||
mutable int16_t axis_w = 0; // the plot's left edge from the object's, as last drawn
|
||||
};
|
||||
|
||||
// The step, 1 / 2 / 5 x 10^k units, that cuts `span` into at most `ticks` parts.
|
||||
static int niceStep(int span, int unit, int ticks) {
|
||||
for (int s = unit; ; s *= 10)
|
||||
for (int m : { 1, 2, 5 }) if (s * m * ticks >= span) return s * m;
|
||||
}
|
||||
|
||||
struct Scale { int lo, hi, step; };
|
||||
|
||||
// lo..hi widened to whole steps.
|
||||
static Scale scaleFor(int lo, int hi, int min_span, int unit, bool from_zero, int ticks) {
|
||||
if (from_zero && lo > 0) lo = 0;
|
||||
if (hi - lo < min_span) {
|
||||
if (from_zero) hi = lo + min_span;
|
||||
else { const int mid = (hi + lo) / 2; lo = mid - min_span / 2; hi = lo + min_span; }
|
||||
}
|
||||
Scale s;
|
||||
s.step = niceStep(hi - lo, unit, ticks);
|
||||
s.lo = (lo >= 0 ? lo / s.step : -((-lo + s.step - 1) / s.step)) * s.step;
|
||||
s.hi = (hi >= 0 ? (hi + s.step - 1) / s.step : -(-hi / s.step)) * s.step;
|
||||
if (s.hi == s.lo) s.hi = s.lo + s.step;
|
||||
return s;
|
||||
}
|
||||
|
||||
static void text(lv_layer_t* layer, const char* t, int32_t x1, int32_t y1, int32_t x2, lv_text_align_t al, uint32_t col) {
|
||||
lv_draw_label_dsc_t d;
|
||||
lv_draw_label_dsc_init(&d);
|
||||
d.font = THEME_FONT_SMALL;
|
||||
d.color = lv_color_hex(col);
|
||||
d.align = al;
|
||||
d.text = t;
|
||||
d.text_local = 1;
|
||||
lv_area_t r = { x1, y1, x2, y1 + lv_font_get_line_height(d.font) };
|
||||
lv_draw_label(layer, &d, &r);
|
||||
}
|
||||
|
||||
static void hline(lv_layer_t* layer, int32_t x1, int32_t x2, int32_t y, uint32_t col, int dash) {
|
||||
lv_draw_line_dsc_t d;
|
||||
lv_draw_line_dsc_init(&d);
|
||||
d.color = lv_color_hex(col);
|
||||
d.width = 1;
|
||||
d.dash_width = d.dash_gap = dash;
|
||||
d.p1.x = x1; d.p2.x = x2; d.p1.y = d.p2.y = y;
|
||||
lv_draw_line(layer, &d);
|
||||
}
|
||||
|
||||
static int textW(const char* t) {
|
||||
lv_point_t sz;
|
||||
lv_text_get_size(&sz, t, THEME_FONT_SMALL, 0, 0, LV_COORD_MAX, LV_TEXT_FLAG_NONE);
|
||||
return sz.x;
|
||||
}
|
||||
|
||||
// The values up the left of y_top..y_bot and a grid line across to x2 at
|
||||
// each; returns where the plot starts. `fmt` writes a value.
|
||||
static int32_t axis(lv_layer_t* layer, int32_t x1, int32_t x2, int32_t y_top, int32_t y_bot, const Scale& s,
|
||||
void (*fmt)(int v, int step, char* out, size_t n), uint32_t grid) {
|
||||
char t[16];
|
||||
int w = 0;
|
||||
for (int v = s.lo; v <= s.hi; v += s.step) { fmt(v, s.step, t, sizeof(t)); const int tw = textW(t); if (tw > w) w = tw; }
|
||||
const int32_t px = x1 + w + 6;
|
||||
const int lh = lv_font_get_line_height(THEME_FONT_SMALL);
|
||||
for (int v = s.lo; v <= s.hi; v += s.step) {
|
||||
const int32_t y = y_bot - (int32_t)(v - s.lo) * (y_bot - y_top) / (s.hi - s.lo);
|
||||
hline(layer, px, x2, y, grid, 0);
|
||||
fmt(v, s.step, t, sizeof(t));
|
||||
text(layer, t, x1, y - lh / 2, px - 6, LV_TEXT_ALIGN_RIGHT, theme::TEXT_MUTED);
|
||||
}
|
||||
return px;
|
||||
}
|
||||
|
||||
// Where the plot is inside the object's area.
|
||||
struct Plot { int32_t x1, x2, y1, y2; };
|
||||
|
||||
static Plot plotOf(const Chart& c, const lv_area_t& a, int32_t px) {
|
||||
const int lh = lv_font_get_line_height(THEME_FONT_SMALL);
|
||||
return { px, a.x2 - 2, a.y1 + c.top + lh / 2, a.y2 - lh - 2 };
|
||||
}
|
||||
|
||||
static int32_t xOf(const Chart& c, const Plot& p, int i) {
|
||||
const int32_t w = p.x2 - p.x1;
|
||||
if (c.spaced) return p.x1 + (int32_t)c.x[i] * w / 10000;
|
||||
return p.x2 - (int32_t)(c.n - 1 - i) * w / (c.cap > 1 ? c.cap - 1 : 1);
|
||||
}
|
||||
|
||||
static void onDraw(lv_event_t* e) {
|
||||
lv_obj_t* o = (lv_obj_t*)lv_event_get_target(e);
|
||||
const Chart& c = *(const Chart*)lv_event_get_user_data(e);
|
||||
lv_layer_t* layer = lv_event_get_layer(e);
|
||||
lv_area_t a;
|
||||
lv_obj_get_coords(o, &a);
|
||||
const int lh = lv_font_get_line_height(THEME_FONT_SMALL);
|
||||
const uint32_t grid = theme::mix(theme::TEXT_MUTED, c.bg, 22);
|
||||
|
||||
int lo = c.n ? c.v[0] : 0, hi = lo;
|
||||
for (int i = 0; i < c.n; i++) { if (c.v[i] < lo) lo = c.v[i]; if (c.v[i] > hi) hi = c.v[i]; }
|
||||
const Scale s = scaleFor(lo, hi, c.min_span, c.unit, c.from_zero || c.bars, 3);
|
||||
const int32_t y_top = a.y1 + c.top + lh / 2, y_bot = a.y2 - lh - 2;
|
||||
const int32_t px = c.n ? axis(layer, a.x1, a.x2 - 2, y_top, y_bot, s, c.axis, grid) : a.x1 + 2;
|
||||
const Plot p = plotOf(c, a, px);
|
||||
c.axis_w = (int16_t)(px - a.x1);
|
||||
|
||||
// Along the bottom, spread evenly.
|
||||
int nu = 0;
|
||||
while (nu < 5 && c.under[nu]) nu++;
|
||||
for (int k = 0; k < nu; k++) {
|
||||
const int32_t x = nu > 1 ? p.x1 + (p.x2 - p.x1) * k / (nu - 1) : p.x1;
|
||||
const lv_text_align_t al = k == 0 ? LV_TEXT_ALIGN_LEFT : k == nu - 1 ? LV_TEXT_ALIGN_RIGHT : LV_TEXT_ALIGN_CENTER;
|
||||
const int32_t x1 = k == 0 ? x : k == nu - 1 ? x - 60 : x - 30;
|
||||
text(layer, c.under[k], x1, p.y2 + 2, x1 + 59, al, theme::TEXT_MUTED);
|
||||
}
|
||||
|
||||
if (c.n < (c.bars ? 1 : 2)) { // nothing yet: a dotted baseline
|
||||
hline(layer, p.x1, p.x2, p.y2, theme::TEXT_MUTED, 2);
|
||||
return;
|
||||
}
|
||||
auto Y = [&](int v) { return (int32_t)(p.y2 - (int32_t)(v - s.lo) * (p.y2 - p.y1) / (s.hi - s.lo)); };
|
||||
|
||||
if (c.bars) { // a bar a sample, a pixel apart
|
||||
const int pitch = ((p.x2 - p.x1) + c.cap / 2) / c.cap, bw = pitch > 1 ? pitch - 1 : 1;
|
||||
lv_draw_rect_dsc_t rd;
|
||||
lv_draw_rect_dsc_init(&rd);
|
||||
for (int i = 0; i < c.n; i++) {
|
||||
const int32_t y = Y(c.v[i]);
|
||||
if (y >= p.y2) continue; // nothing (none heard, or the bottom of the scale)
|
||||
rd.bg_color = lv_color_hex(i == c.touch ? theme::TEXT : i == c.mark ? theme::ACCENT : c.bar);
|
||||
const int32_t x = xOf(c, p, i);
|
||||
lv_area_t r = { x - bw + 1, y, x, p.y2 };
|
||||
lv_draw_rect(layer, &rd, &r);
|
||||
}
|
||||
} else {
|
||||
lv_draw_line_dsc_t fill; // a column at a time under the line (triangles would leave seams)
|
||||
lv_draw_line_dsc_init(&fill);
|
||||
fill.color = lv_color_hex(theme::mix(theme::ACCENT, c.bg, 14));
|
||||
fill.width = 1;
|
||||
for (int i = 1; i < c.n; i++) {
|
||||
const int32_t xa = xOf(c, p, i - 1), xb = xOf(c, p, i), ya = Y(c.v[i - 1]), yb = Y(c.v[i]);
|
||||
for (int32_t x = (i == 1 ? xa : xa + 1); x <= xb; x++) {
|
||||
fill.p1.x = fill.p2.x = x;
|
||||
fill.p1.y = xb > xa ? ya + (yb - ya) * (x - xa) / (xb - xa) : yb;
|
||||
fill.p2.y = p.y2;
|
||||
if (fill.p1.y < fill.p2.y) lv_draw_line(layer, &fill);
|
||||
}
|
||||
}
|
||||
lv_draw_line_dsc_t ld;
|
||||
lv_draw_line_dsc_init(&ld);
|
||||
ld.color = lv_color_hex(theme::ACCENT);
|
||||
ld.width = 2;
|
||||
ld.round_start = ld.round_end = 1;
|
||||
for (int i = 1; i < c.n; i++) {
|
||||
ld.p1.x = xOf(c, p, i - 1); ld.p1.y = Y(c.v[i - 1]);
|
||||
ld.p2.x = xOf(c, p, i); ld.p2.y = Y(c.v[i]);
|
||||
lv_draw_line(layer, &ld);
|
||||
}
|
||||
}
|
||||
|
||||
if (c.touch < 0 || c.touch >= c.n) return;
|
||||
// The sample under the finger: a line down, a dot on the line, a pill over the plot.
|
||||
const int i = c.touch;
|
||||
const int32_t x = xOf(c, p, i), y = Y(c.v[i]);
|
||||
lv_draw_line_dsc_t vl;
|
||||
lv_draw_line_dsc_init(&vl);
|
||||
vl.color = lv_color_hex(theme::TEXT_MUTED);
|
||||
vl.width = 1;
|
||||
vl.p1.x = vl.p2.x = x; vl.p1.y = a.y1 + c.top; vl.p2.y = p.y2;
|
||||
lv_draw_line(layer, &vl);
|
||||
if (!c.bars) {
|
||||
lv_draw_rect_dsc_t dd;
|
||||
lv_draw_rect_dsc_init(&dd);
|
||||
dd.radius = LV_RADIUS_CIRCLE;
|
||||
dd.bg_color = lv_color_hex(theme::TEXT);
|
||||
dd.border_color = lv_color_hex(c.bg);
|
||||
dd.border_width = 2;
|
||||
lv_area_t da = { x - 5, y - 5, x + 5, y + 5 };
|
||||
lv_draw_rect(layer, &dd, &da);
|
||||
}
|
||||
char val[24] = "", at[24] = "", t[52];
|
||||
if (c.value) c.value(c, i, val, sizeof(val));
|
||||
if (c.where) c.where(c, i, at, sizeof(at));
|
||||
snprintf(t, sizeof(t), at[0] ? "%s %s" : "%s", val, at);
|
||||
const int32_t w = textW(t) + 12;
|
||||
int32_t bx = x - w / 2;
|
||||
if (bx < a.x1) bx = a.x1;
|
||||
if (bx + w > a.x2) bx = a.x2 - w;
|
||||
lv_draw_rect_dsc_t bg;
|
||||
lv_draw_rect_dsc_init(&bg);
|
||||
bg.radius = theme::RADIUS_SM;
|
||||
bg.bg_color = lv_color_hex(theme::SURFACE_2);
|
||||
lv_area_t ba = { bx, a.y1 + c.top, bx + w - 1, a.y1 + c.top + lh + 1 };
|
||||
lv_draw_rect(layer, &bg, &ba);
|
||||
text(layer, t, bx, a.y1 + c.top + 1, bx + w - 1, LV_TEXT_ALIGN_CENTER, theme::TEXT);
|
||||
}
|
||||
|
||||
// The finger: the nearest sample along the width, while it's down.
|
||||
static void onTouch(lv_event_t* e) {
|
||||
const lv_event_code_t code = lv_event_get_code(e);
|
||||
lv_obj_t* o = (lv_obj_t*)lv_event_get_current_target(e);
|
||||
Chart& c = *(Chart*)lv_event_get_user_data(e);
|
||||
int16_t t = c.touch;
|
||||
if (code == LV_EVENT_PRESSED || code == LV_EVENT_PRESSING) {
|
||||
lv_indev_t* in = lv_indev_active();
|
||||
if (!in || !c.n) return;
|
||||
lv_point_t pt;
|
||||
lv_indev_get_point(in, &pt);
|
||||
lv_area_t a;
|
||||
lv_obj_get_coords(o, &a);
|
||||
const Plot p = plotOf(c, a, a.x1 + c.axis_w);
|
||||
int best = -1;
|
||||
int32_t bd = 0;
|
||||
for (int i = 0; i < c.n; i++) {
|
||||
const int32_t d = LV_ABS(xOf(c, p, i) - pt.x);
|
||||
if (best < 0 || d < bd) { best = i; bd = d; }
|
||||
}
|
||||
t = (int16_t)best;
|
||||
} else if (code == LV_EVENT_RELEASED || code == LV_EVENT_PRESS_LOST) {
|
||||
t = -1;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
if (t != c.touch) { c.touch = t; lv_obj_invalidate(o); }
|
||||
}
|
||||
|
||||
// The chart's object, `h` tall, drawing `c`.
|
||||
static lv_obj_t* create(lv_obj_t* parent, Chart& c, int h) {
|
||||
lv_obj_t* o = lv_obj_create(parent);
|
||||
lv_obj_remove_style_all(o);
|
||||
lv_obj_remove_flag(o, LV_OBJ_FLAG_SCROLLABLE);
|
||||
lv_obj_set_width(o, LV_PCT(100));
|
||||
lv_obj_set_height(o, h);
|
||||
c.touch = -1;
|
||||
lv_obj_add_event_cb(o, onDraw, LV_EVENT_DRAW_MAIN_END, &c);
|
||||
lv_obj_add_event_cb(o, onTouch, LV_EVENT_ALL, &c);
|
||||
return o;
|
||||
}
|
||||
|
||||
// "35 min ago", "2 h 10 min ago", "now".
|
||||
static void ago(uint32_t mins, char* out, size_t n) {
|
||||
if (!mins) snprintf(out, n, "now");
|
||||
else if (mins < 60) snprintf(out, n, "%lu min ago", (unsigned long)mins);
|
||||
else if (mins % 60) snprintf(out, n, "%lu h %lu min ago", (unsigned long)(mins / 60), (unsigned long)(mins % 60));
|
||||
else snprintf(out, n, "%lu h ago", (unsigned long)(mins / 60));
|
||||
}
|
||||
|
||||
} // namespace chart
|
||||
@@ -36,83 +36,60 @@ static uint32_t totalRecv() {
|
||||
}
|
||||
|
||||
// One chart's samples as drawn: a copy, so the draw callback needn't know
|
||||
// which ring (of which size) they came from.
|
||||
struct Chart { int16_t v[96]; int n, cap, min_span; bool bars; };
|
||||
static Chart s_charts[4];
|
||||
// which ring (of which size) they came from. What the axis and the pill
|
||||
// say is set once (setupCharts()).
|
||||
static chart::Chart s_charts[4];
|
||||
template <int N>
|
||||
static Chart* chartOf(int k, const uicore::History<N>& h, int min_span, bool bars) {
|
||||
Chart& c = s_charts[k];
|
||||
c.n = h.n; c.cap = N; c.min_span = min_span; c.bars = bars;
|
||||
static void chartOf(int k, const uicore::History<N>& h) {
|
||||
chart::Chart& c = s_charts[k];
|
||||
c.n = h.n; c.cap = N;
|
||||
for (int i = 0; i < h.n; i++) c.v[i] = h.at(i);
|
||||
return &c;
|
||||
}
|
||||
|
||||
// A history in its box, newest at the right: a line over a faint fill, or
|
||||
// bars from the bottom, scaled between the samples' own min and max (at
|
||||
// least min_span apart, so a steady reading stays flat).
|
||||
static void onChartDraw(lv_event_t* e) {
|
||||
lv_obj_t* o = (lv_obj_t*)lv_event_get_target(e);
|
||||
const Chart& c = *(const Chart*)lv_event_get_user_data(e);
|
||||
lv_layer_t* layer = lv_event_get_layer(e);
|
||||
lv_area_t a;
|
||||
lv_obj_get_coords(o, &a);
|
||||
const int x0 = a.x1 + 2, w = a.x2 - a.x1 - 4, y0 = a.y1 + 3, h = a.y2 - a.y1 - 5, base = a.y2 - 1;
|
||||
lv_draw_line_dsc_t ld;
|
||||
lv_draw_line_dsc_init(&ld);
|
||||
if (c.n < (c.bars ? 1 : 2)) { // nothing yet: a dotted baseline
|
||||
ld.color = lv_color_hex(theme::TEXT_MUTED);
|
||||
ld.width = 1; ld.dash_width = 2; ld.dash_gap = 4;
|
||||
ld.p1.x = x0; ld.p2.x = x0 + w; ld.p1.y = ld.p2.y = base - 1;
|
||||
lv_draw_line(layer, &ld);
|
||||
return;
|
||||
}
|
||||
int lo = c.bars ? 0 : c.v[0], hi = c.v[0];
|
||||
for (int i = 0; i < c.n; i++) { if (c.v[i] < lo) lo = c.v[i]; if (c.v[i] > hi) hi = c.v[i]; }
|
||||
if (hi - lo < c.min_span) {
|
||||
if (c.bars) hi = lo + c.min_span;
|
||||
else { const int mid = (hi + lo) / 2; lo = mid - c.min_span / 2; hi = lo + c.min_span; }
|
||||
}
|
||||
auto X = [&](int i) { return (int32_t)(x0 + w - (c.n - 1 - i) * w / (c.cap - 1)); };
|
||||
auto Y = [&](int v) { return (int32_t)(y0 + h - (v - lo) * h / (hi - lo)); };
|
||||
ld.color = lv_color_hex(theme::ACCENT);
|
||||
if (c.bars) { // a bar a sample, a pixel apart
|
||||
const int pitch = (w + c.cap / 2) / c.cap, bw = pitch > 2 ? pitch - 1 : 1;
|
||||
lv_draw_rect_dsc_t rd;
|
||||
lv_draw_rect_dsc_init(&rd);
|
||||
rd.bg_color = lv_color_hex(theme::ACCENT);
|
||||
rd.bg_opa = LV_OPA_70;
|
||||
for (int i = 0; i < c.n; i++) {
|
||||
if (c.v[i] <= 0) continue;
|
||||
lv_area_t r = { X(i) - bw + 1, Y(c.v[i]), X(i), base };
|
||||
lv_draw_rect(layer, &rd, &r);
|
||||
}
|
||||
return;
|
||||
}
|
||||
lv_draw_line_dsc_t fill;
|
||||
lv_draw_line_dsc_init(&fill);
|
||||
fill.color = lv_color_hex(theme::ACCENT);
|
||||
fill.opa = LV_OPA_20;
|
||||
fill.width = 1;
|
||||
ld.width = 2;
|
||||
ld.round_start = ld.round_end = 1;
|
||||
for (int i = 1; i < c.n; i++) {
|
||||
const int32_t xa = X(i - 1), xb = X(i), ya = Y(c.v[i - 1]), yb = Y(c.v[i]);
|
||||
for (int32_t x = (i == 1 ? xa : xa + 1); x <= xb; x++) {
|
||||
fill.p1.x = fill.p2.x = x;
|
||||
fill.p1.y = xb > xa ? ya + (yb - ya) * (x - xa) / (xb - xa) : yb;
|
||||
fill.p2.y = base;
|
||||
lv_draw_line(layer, &fill);
|
||||
}
|
||||
ld.p1.x = xa; ld.p1.y = ya; ld.p2.x = xb; ld.p2.y = yb;
|
||||
lv_draw_line(layer, &ld);
|
||||
// A sample's age: the charts sample every 5 minutes, SNR every packet.
|
||||
static void whenAgo(const chart::Chart& c, int i, char* out, size_t n) {
|
||||
chart::ago((uint32_t)(c.n - 1 - i) * (s_hist.INTERVAL_MS / 60000UL), out, n);
|
||||
}
|
||||
static void setupCharts() {
|
||||
using chart::Chart;
|
||||
static const char* const HOURS[5] = { "8 h", "6 h", "4 h", "2 h", "now" };
|
||||
for (int k = 0; k < 4; k++) {
|
||||
Chart& c = s_charts[k];
|
||||
c.where = whenAgo;
|
||||
for (int j = 0; j < 5; j++) c.under[j] = HOURS[j];
|
||||
}
|
||||
Chart& b = s_charts[0]; // mV, in steps of 10 mV and up
|
||||
b.min_span = 100; b.unit = 10;
|
||||
b.axis = [](int v, int step, char* o, size_t n) {
|
||||
if (step % 100 == 0) snprintf(o, n, "%d.%d V", v / 1000, v % 1000 / 100);
|
||||
else snprintf(o, n, "%d.%02d", v / 1000, v % 1000 / 10);
|
||||
};
|
||||
b.value = [](const Chart& c, int i, char* o, size_t n) { snprintf(o, n, "%.2f V", c.v[i] / 1000.0f); };
|
||||
Chart& f = s_charts[1]; // dBm
|
||||
f.min_span = 6;
|
||||
f.axis = [](int v, int, char* o, size_t n) { snprintf(o, n, "%d", v); };
|
||||
f.value = [](const Chart& c, int i, char* o, size_t n) { snprintf(o, n, "%d dBm", c.v[i]); };
|
||||
Chart& r = s_charts[2]; // dB x4, a sample a packet
|
||||
r.min_span = 20; r.unit = 4;
|
||||
r.axis = [](int v, int, char* o, size_t n) { snprintf(o, n, "%d", v / 4); };
|
||||
r.value = [](const Chart& c, int i, char* o, size_t n) { snprintf(o, n, "%.1f dB", c.v[i] / 4.0f); };
|
||||
r.where = [](const Chart& c, int i, char* o, size_t n) {
|
||||
const int k = c.n - 1 - i;
|
||||
if (!k) snprintf(o, n, "last packet"); else snprintf(o, n, "%d packet%s back", k, k == 1 ? "" : "s");
|
||||
};
|
||||
static const char* const PKTS[3] = { "32 back", "16", "last" };
|
||||
for (int j = 0; j < 5; j++) r.under[j] = j < 3 ? PKTS[j] : nullptr;
|
||||
Chart& t = s_charts[3]; // packets per interval
|
||||
t.bars = true; t.min_span = 4;
|
||||
t.axis = [](int v, int, char* o, size_t n) { snprintf(o, n, "%d", v); };
|
||||
t.value = [](const Chart& c, int i, char* o, size_t n) { snprintf(o, n, "%d packet%s", c.v[i], c.v[i] == 1 ? "" : "s"); };
|
||||
}
|
||||
|
||||
// A chart card: the title and the value now, the chart, the span under it.
|
||||
static void chartCard(lv_obj_t* parent, int k, const char* title, const char* from) {
|
||||
// A chart card: the title and the value now over the chart.
|
||||
static void chartCard(lv_obj_t* parent, int k, const char* title) {
|
||||
lv_obj_t* card = infoCard(parent);
|
||||
lv_obj_set_style_pad_ver(card, 6, 0);
|
||||
lv_obj_set_style_pad_row(card, 2, 0);
|
||||
lv_obj_set_style_pad_row(card, 4, 0);
|
||||
lv_obj_t* r = lv_obj_create(card);
|
||||
lv_obj_remove_style_all(r);
|
||||
fillWidth(r);
|
||||
@@ -120,19 +97,7 @@ static void chartCard(lv_obj_t* parent, int k, const char* title, const char* fr
|
||||
lv_obj_align(label(r, title, THEME_FONT_SMALL, theme::TEXT_MUTED), LV_ALIGN_LEFT_MID, 0, 0);
|
||||
s_ch_now[k] = label(r, "", THEME_FONT_BODY, theme::TEXT);
|
||||
lv_obj_align(s_ch_now[k], LV_ALIGN_RIGHT_MID, 0, 0);
|
||||
lv_obj_t* ch = lv_obj_create(card);
|
||||
lv_obj_remove_style_all(ch);
|
||||
lv_obj_remove_flag(ch, LV_OBJ_FLAG_CLICKABLE);
|
||||
fillWidth(ch);
|
||||
lv_obj_set_height(ch, 48);
|
||||
lv_obj_add_event_cb(ch, onChartDraw, LV_EVENT_DRAW_MAIN_END, &s_charts[k]);
|
||||
s_ch_obj[k] = ch;
|
||||
lv_obj_t* ax = lv_obj_create(card);
|
||||
lv_obj_remove_style_all(ax);
|
||||
fillWidth(ax);
|
||||
lv_obj_set_height(ax, LV_SIZE_CONTENT);
|
||||
lv_obj_align(label(ax, from, THEME_FONT_SMALL, theme::TEXT_MUTED), LV_ALIGN_LEFT_MID, 0, 0);
|
||||
lv_obj_align(label(ax, "now", THEME_FONT_SMALL, theme::TEXT_MUTED), LV_ALIGN_RIGHT_MID, 0, 0);
|
||||
s_ch_obj[k] = chart::create(card, s_charts[k], 96);
|
||||
}
|
||||
|
||||
static void extraRow(diag::Row* rows, int& n, const char* lbl, const char* fmt, ...) {
|
||||
@@ -251,8 +216,10 @@ static lv_obj_t* s_noise_status = nullptr;
|
||||
|
||||
static void onNoiseRun(lv_event_t* e) { (void)e; s_ui->diagNoiseRun(); }
|
||||
|
||||
// A sweep as bars from -130 dBm up (1.5 px a dB), the step nearest the mesh's
|
||||
// frequency in the accent colour, then its quietest / loudest / mesh values.
|
||||
// A sweep as bars (Chart.h), the step nearest the mesh's frequency in the
|
||||
// accent, then its quietest / loudest values.
|
||||
static chart::Chart s_sweep_chart;
|
||||
static char s_sweep_under[3][12];
|
||||
static void plotSweep(lv_obj_t* list, const char* title, const Sweep& sw, float mesh_f) {
|
||||
sectionTitle(list, title);
|
||||
int lo = 0, hi = 0;
|
||||
@@ -260,23 +227,24 @@ static void plotSweep(lv_obj_t* list, const char* title, const Sweep& sw, float
|
||||
if (sw.v[i] < sw.v[lo]) lo = i;
|
||||
if (sw.v[i] > sw.v[hi]) hi = i;
|
||||
}
|
||||
lv_obj_t* plot = lv_obj_create(list);
|
||||
lv_obj_remove_style_all(plot);
|
||||
lv_obj_set_size(plot, sw.n * 3, 100);
|
||||
lv_obj_set_style_bg_color(plot, lv_color_hex(theme::SURFACE), 0);
|
||||
lv_obj_set_style_bg_opa(plot, LV_OPA_COVER, 0);
|
||||
lv_obj_remove_flag(plot, LV_OBJ_FLAG_SCROLLABLE);
|
||||
int mesh_i = (int)lroundf((mesh_f - sw.f0) / sw.step);
|
||||
for (int i = 0; i < sw.n; i++) {
|
||||
int h = (sw.v[i] + 130) * 3 / 2;
|
||||
h = h < 1 ? 1 : (h > 100 ? 100 : h);
|
||||
lv_obj_t* bar = lv_obj_create(plot);
|
||||
lv_obj_remove_style_all(bar);
|
||||
lv_obj_set_size(bar, 2, h);
|
||||
lv_obj_set_pos(bar, i * 3, 100 - h);
|
||||
lv_obj_set_style_bg_color(bar, lv_color_hex(i == mesh_i ? theme::ACCENT : theme::TEXT_MUTED), 0);
|
||||
lv_obj_set_style_bg_opa(bar, LV_OPA_COVER, 0);
|
||||
}
|
||||
chart::Chart& c = s_sweep_chart;
|
||||
c = chart::Chart();
|
||||
c.n = c.cap = sw.n;
|
||||
for (int i = 0; i < sw.n; i++) c.v[i] = sw.v[i];
|
||||
c.bars = true;
|
||||
c.bar = theme::TEXT_MUTED;
|
||||
c.mark = (int16_t)lroundf((mesh_f - sw.f0) / sw.step);
|
||||
c.min_span = 10;
|
||||
c.axis = [](int v, int, char* o, size_t n) { snprintf(o, n, "%d", v); };
|
||||
c.value = [](const chart::Chart& c, int i, char* o, size_t n) { snprintf(o, n, "%d dBm", c.v[i]); };
|
||||
c.where = [](const chart::Chart&, int i, char* o, size_t n) { snprintf(o, n, "%.3f MHz", s_near.f0 + s_near.step * i); };
|
||||
snprintf(s_sweep_under[0], sizeof(s_sweep_under[0]), "%.2f", sw.f0);
|
||||
snprintf(s_sweep_under[1], sizeof(s_sweep_under[1]), "%.3f", mesh_f);
|
||||
snprintf(s_sweep_under[2], sizeof(s_sweep_under[2]), "%.2f MHz", sw.f0 + sw.step * (sw.n - 1));
|
||||
for (int j = 0; j < 3; j++) c.under[j] = s_sweep_under[j];
|
||||
lv_obj_t* box = infoCard(list);
|
||||
lv_obj_set_style_pad_ver(box, 6, 0);
|
||||
chart::create(box, c, 120);
|
||||
lv_obj_t* card = infoCard(list);
|
||||
char v[40];
|
||||
snprintf(v, sizeof(v), "%.3f MHz: %d dBm", sw.f0 + sw.step * lo, sw.v[lo]);
|
||||
@@ -385,10 +353,11 @@ void UITask::buildDiag() {
|
||||
|
||||
s_noise_status = nullptr;
|
||||
if (s_tab == TAB_HISTORY) {
|
||||
chartCard(s_list, 0, "Battery", "8 h ago");
|
||||
chartCard(s_list, 1, "Noise floor", "8 h ago");
|
||||
chartCard(s_list, 2, "SNR, last packets", "32 packets ago");
|
||||
chartCard(s_list, 3, "Packets heard, per 5 min", "8 h ago");
|
||||
setupCharts();
|
||||
chartCard(s_list, 0, "Battery");
|
||||
chartCard(s_list, 1, "Noise floor (dBm)");
|
||||
chartCard(s_list, 2, "SNR of the last packets (dB)");
|
||||
chartCard(s_list, 3, "Packets heard, per 5 min");
|
||||
fillHistory();
|
||||
return;
|
||||
}
|
||||
@@ -491,10 +460,10 @@ void UITask::diagSection(int sec) {
|
||||
void UITask::fillHistory() {
|
||||
using namespace diagview;
|
||||
s_hist_shown = s_hist.version;
|
||||
chartOf(0, s_hist.batt, 20, false);
|
||||
chartOf(1, s_hist.noise, 6, false);
|
||||
chartOf(2, s_hist.snr, 20, false);
|
||||
chartOf(3, s_hist.traffic, 4, true);
|
||||
chartOf(0, s_hist.batt);
|
||||
chartOf(1, s_hist.noise);
|
||||
chartOf(2, s_hist.snr);
|
||||
chartOf(3, s_hist.traffic);
|
||||
for (int k = 0; k < 4; k++) lv_obj_invalidate(s_ch_obj[k]);
|
||||
setTextFmt(s_ch_now[0], "%.2f V", (_batt_mv ? _batt_mv : getBattMilliVolts()) / 1000.0f);
|
||||
const int nf = (int)radio_driver.getNoiseFloor();
|
||||
|
||||
@@ -977,7 +977,7 @@ void UITask::navToolsPopup() {
|
||||
_nav_trail_lbl = label(list, "", THEME_FONT_SMALL, theme::TEXT);
|
||||
profileview::s_prof = trailprofile::Sampler(); // its height profile, as it is now
|
||||
profileview::s_prof.feedAll(_core->trail.store());
|
||||
profileChart(list, 72);
|
||||
profileChart(list, 120);
|
||||
lv_obj_t* r = buttonBar(list);
|
||||
_nav_trail_btn = toolButton(r, "", navmap::TL_TRAIL_TOGGLE, true);
|
||||
toolButton(r, LV_SYMBOL_SAVE " Save", navmap::TL_TRAIL_SAVE, false);
|
||||
@@ -1251,7 +1251,7 @@ void UITask::savedTrailPopup(int idx) {
|
||||
if (ok) snprintf(info, sizeof(info), "%s - %s - %d points", dist, dur, pts);
|
||||
else snprintf(info, sizeof(info), "Can't read this file");
|
||||
label(panel, info, THEME_FONT_BODY, theme::TEXT);
|
||||
if (ok) profileChart(panel, 60);
|
||||
if (ok) profileChart(panel, 104);
|
||||
if (!_core->trail.store().empty())
|
||||
label(panel, "Loading replaces the trail on the map.", THEME_FONT_SMALL, theme::TEXT_MUTED);
|
||||
lv_obj_t* r = buttonBar(panel);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#pragma once
|
||||
// A trail's height profile (ui-core/TrailProfile.h) as a chart: the height
|
||||
// over the distance, a line in the accent over a faint fill, the climb and
|
||||
// the highest and lowest points over it, top left. In the live trail's Map tools section and
|
||||
// A trail's height profile (ui-core/TrailProfile.h) as a chart (Chart.h):
|
||||
// the height over the distance, the climb and the highest and lowest points
|
||||
// over it. In the live trail's Map tools section and
|
||||
// a saved trail's popup. ui-new draws the same on e-ink and the OLED
|
||||
// (TrailScreen.h).
|
||||
//
|
||||
@@ -10,42 +10,12 @@
|
||||
namespace profileview {
|
||||
|
||||
static trailprofile::Sampler s_prof; // what the chart on screen shows
|
||||
static chart::Chart s_chart;
|
||||
static char s_under[3][12];
|
||||
|
||||
static void onDraw(lv_event_t* e) {
|
||||
const trailprofile::Sampler& p = s_prof;
|
||||
if (p.n < 2) return;
|
||||
lv_obj_t* o = (lv_obj_t*)lv_event_get_target(e);
|
||||
lv_layer_t* layer = lv_event_get_layer(e);
|
||||
lv_area_t a;
|
||||
lv_obj_get_coords(o, &a);
|
||||
int lo = p.st.lo, hi = p.st.hi;
|
||||
if (hi - lo < 20) { const int mid = (hi + lo) / 2; lo = mid - 10; hi = mid + 10; }
|
||||
const float total = p.total > 0 ? p.total : 1.0f;
|
||||
const int x0 = a.x1 + 2, w = a.x2 - a.x1 - 4, y0 = a.y1 + 20, h = a.y2 - y0 - 2, base = a.y2 - 1; // under the caption
|
||||
auto X = [&](int i) { return (int32_t)(x0 + p.dist[i] / total * w); };
|
||||
auto Y = [&](int i) { return (int32_t)(y0 + h - (p.alt[i] - lo) * h / (hi - lo)); };
|
||||
|
||||
lv_draw_line_dsc_t fill; // under the line, a column at a time (triangles would leave seams)
|
||||
lv_draw_line_dsc_init(&fill);
|
||||
fill.color = lv_color_hex(theme::ACCENT);
|
||||
fill.opa = LV_OPA_20;
|
||||
fill.width = 1;
|
||||
lv_draw_line_dsc_t ld;
|
||||
lv_draw_line_dsc_init(&ld);
|
||||
ld.color = lv_color_hex(theme::ACCENT);
|
||||
ld.width = 2;
|
||||
ld.round_start = ld.round_end = 1;
|
||||
for (int i = 1; i < p.n; i++) {
|
||||
const int32_t xa = X(i - 1), xb = X(i), ya = Y(i - 1), yb = Y(i);
|
||||
for (int32_t x = (i == 1 ? xa : xa + 1); x <= xb; x++) {
|
||||
fill.p1.x = fill.p2.x = x;
|
||||
fill.p1.y = xb > xa ? ya + (yb - ya) * (x - xa) / (xb - xa) : yb;
|
||||
fill.p2.y = base;
|
||||
lv_draw_line(layer, &fill);
|
||||
}
|
||||
ld.p1.x = xa; ld.p1.y = ya; ld.p2.x = xb; ld.p2.y = yb;
|
||||
lv_draw_line(layer, &ld);
|
||||
}
|
||||
static void distText(float m, char* out, size_t n) {
|
||||
if (m < 1000) snprintf(out, n, "%d m", (int)lroundf(m));
|
||||
else snprintf(out, n, "%.1f km", m / 1000.0f);
|
||||
}
|
||||
|
||||
} // namespace profileview
|
||||
@@ -55,16 +25,36 @@ static void onDraw(lv_event_t* e) {
|
||||
static lv_obj_t* profileChart(lv_obj_t* parent, int h) {
|
||||
using namespace profileview;
|
||||
if (!s_prof.st.any || s_prof.n < 2) return nullptr;
|
||||
lv_obj_t* c = lv_obj_create(parent);
|
||||
styleSurface(c, theme::SURFACE);
|
||||
lv_obj_remove_flag(c, LV_OBJ_FLAG_SCROLLABLE);
|
||||
lv_obj_remove_flag(c, LV_OBJ_FLAG_CLICKABLE);
|
||||
fillWidth(c);
|
||||
lv_obj_set_height(c, h);
|
||||
lv_obj_set_style_radius(c, theme::RADIUS_SM, 0);
|
||||
lv_obj_add_event_cb(c, onDraw, LV_EVENT_DRAW_MAIN_END, NULL);
|
||||
chart::Chart& c = s_chart;
|
||||
c = chart::Chart();
|
||||
const float total = s_prof.total > 0 ? s_prof.total : 1.0f;
|
||||
c.n = s_prof.n;
|
||||
c.spaced = true;
|
||||
for (int i = 0; i < c.n; i++) {
|
||||
c.v[i] = s_prof.alt[i];
|
||||
c.x[i] = (uint16_t)lroundf(s_prof.dist[i] / total * 10000);
|
||||
}
|
||||
c.min_span = 20;
|
||||
c.axis = [](int v, int, char* o, size_t n) { snprintf(o, n, "%d m", v); };
|
||||
c.value = [](const chart::Chart& c, int i, char* o, size_t n) { snprintf(o, n, "%d m", c.v[i]); };
|
||||
c.where = [](const chart::Chart& c, int i, char* o, size_t n) { (void)c; distText(s_prof.dist[i], o, n); };
|
||||
distText(0, s_under[0], sizeof(s_under[0]));
|
||||
distText(total / 2, s_under[1], sizeof(s_under[1]));
|
||||
distText(total, s_under[2], sizeof(s_under[2]));
|
||||
for (int j = 0; j < 3; j++) c.under[j] = s_under[j];
|
||||
const int lh = lv_font_get_line_height(THEME_FONT_SMALL);
|
||||
c.top = lh + 2; // the caption
|
||||
lv_obj_t* box = lv_obj_create(parent);
|
||||
styleSurface(box, theme::SURFACE);
|
||||
lv_obj_remove_flag(box, LV_OBJ_FLAG_SCROLLABLE);
|
||||
lv_obj_remove_flag(box, LV_OBJ_FLAG_CLICKABLE);
|
||||
fillWidth(box);
|
||||
lv_obj_set_height(box, h);
|
||||
lv_obj_set_style_radius(box, theme::RADIUS_SM, 0);
|
||||
lv_obj_set_style_pad_all(box, 6, 0);
|
||||
lv_obj_t* o = chart::create(box, c, h - 12);
|
||||
char t[64];
|
||||
snprintf(t, sizeof(t), "Climb %d m - highest %d m - lowest %d m", s_prof.st.gain, s_prof.st.hi, s_prof.st.lo);
|
||||
lv_obj_align(label(c, t, THEME_FONT_SMALL, theme::TEXT_MUTED), LV_ALIGN_TOP_LEFT, 6, 2);
|
||||
return c;
|
||||
lv_obj_align(label(o, t, THEME_FONT_SMALL, theme::TEXT_MUTED), LV_ALIGN_TOP_LEFT, 0, 0);
|
||||
return box;
|
||||
}
|
||||
|
||||
@@ -3530,6 +3530,7 @@ void UITask::nodeAction(uint8_t action) {
|
||||
|
||||
static lv_obj_t* switchRow(lv_obj_t* parent, const char* text, const char* sub, uint8_t* pref); // below
|
||||
#include "MapScreen.h"
|
||||
#include "Chart.h"
|
||||
#include "ProfileView.h"
|
||||
#include "NavMap.h"
|
||||
#include "MapAreas.h"
|
||||
@@ -4641,6 +4642,37 @@ extern "C" EMSCRIPTEN_KEEPALIVE void sim_open(const char* name) {
|
||||
if (!strcmp(name, "region0")) { s_ui->mapRegionPopup(0); return; }
|
||||
if (!strcmp(name, "advert")) { s_ui->advertPopup(); return; }
|
||||
if (!strcmp(name, "trailsave")) { s_ui->navToolAction(navmap::TL_TRAIL_SAVE); return; } // Map tools > Save
|
||||
if (!strcmp(name, "histdemo")) { // 8 h of made-up history, for the charts
|
||||
using namespace diagview;
|
||||
for (int i = 0; i < 96; i++) {
|
||||
s_hist.batt.push((int16_t)(4150 - i * 4 + (i % 7) * 3));
|
||||
s_hist.noise.push((int16_t)(-112 + (i % 11 == 0 ? 6 : 0) + (i / 24)));
|
||||
s_hist.traffic.push((int16_t)((i * 37) % 13 + (i % 16 == 0 ? 20 : 0)));
|
||||
}
|
||||
for (int i = 0; i < 32; i++) s_hist.snr.push((int16_t)(4 * (8 - (i * 7) % 19)));
|
||||
s_hist.version++;
|
||||
s_near.f0 = 869.525f - s_near.step * (s_near.n / 2); // and a noise sweep
|
||||
for (int i = 0; i < s_near.n; i++) s_near.v[i] = (int16_t)(-118 + (i % 9 == 0 ? 7 : (i * 5) % 4));
|
||||
s_floor_med = -116; s_floor_lo = -118;
|
||||
s_noise_have = true;
|
||||
return;
|
||||
}
|
||||
if (!strcmp(name, "profdemo")) { // a made-up height profile in a popup, for the chart
|
||||
using namespace profileview;
|
||||
s_prof = trailprofile::Sampler();
|
||||
for (int i = 0; i < 100; i++) {
|
||||
s_prof.dist[i] = i * 62.0f;
|
||||
s_prof.alt[i] = (int16_t)(420 + 80 * sinf(i / 14.0f) + i * 1.5f);
|
||||
s_prof.st.add(s_prof.alt[i]);
|
||||
}
|
||||
s_prof.n = 100; s_prof.total = 99 * 62.0f;
|
||||
lv_obj_t* box = lv_obj_create(lv_layer_top());
|
||||
styleSurface(box, theme::BG);
|
||||
lv_obj_set_size(box, 304, 132);
|
||||
lv_obj_center(box);
|
||||
profileChart(box, 120);
|
||||
return;
|
||||
}
|
||||
if (!strcmp(name, "savedtrail0")) { navmap::scanTrails(); s_ui->savedTrailPopup(0); return; }
|
||||
if (!strncmp(name, "trail@", 6)) { // "trail@name.trl" from /sdcard/trails: Load, as the saved-trail popup does
|
||||
navmap::scanTrails();
|
||||
|
||||
Reference in New Issue
Block a user