Files
MeshCore-Solo/examples/companion_radio/ui-lvgl/BatteryScreen.h
T
JakubandClaude Opus 5.5 61c75946f6 feat(l2,core): battery curve editor and an L2 feedback round
- Battery curve: NodePrefs batt_curve_mv[11] (voltage at 0..100 %, all
  zeros = built-in LiPo curve, sentinel 0xC0DE0032). ui-core/Battery.h
  uses it for every percentage. L1: Settings > System > Batt curve list
  (Left/Right 10 mV, Reset); L2: Settings > Power > Battery curve chart
  with draggable points and a line at the cell's voltage.
- L2: the Nodes map is the full navigation map plus the nodes, Back
  returns to Nodes; the favourite button is lit while it is one; pinning
  to the Home dial uses a thumbtack (fonts regenerated); minimap dots
  follow the accent; arranging tiles shows where they land while dragging;
  emoji row in Diagnostics > Font.
- Sim hooks: sim_open nodesmap / accent@N / homeedit / battcurve.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 11:25:58 +02:00

170 lines
7.2 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#pragma once
// Settings › Power › Battery curve: the cell voltage at 0, 10, ... 100 %
// (NodePrefs::batt_curve_mv, ui-core/Battery.h) as a chart. A finger takes
// the point nearest it along the % axis and drags it up or down in 10 mV
// steps, kept between its neighbours; the first drag copies the built-in
// curve in, Reset goes back to it. A dashed line marks the cell now, so a
// point can be set against what the battery reads. Included by UITask.cpp.
namespace battview {
static lv_obj_t* s_chart = nullptr;
static lv_obj_t* s_now = nullptr; // "Now 3912 mV 76 %"
static lv_obj_t* s_reset = nullptr; // header button's label
static int s_drag = -1; // point under the finger
static const int PTS = battery::CURVE_PTS;
static const int MV_LO = 3000, MV_HI = 4400; // the chart's voltage range
static const int PAD_L = 44, PAD_R = 12, PAD_T = 10, PAD_B = 22;
static int ptX(const lv_area_t& a, int i) { return a.x1 + PAD_L + i * (lv_area_get_width(&a) - PAD_L - PAD_R) / (PTS - 1); }
static int mvY(const lv_area_t& a, int mv) {
const int h = lv_area_get_height(&a) - PAD_T - PAD_B;
return a.y2 - PAD_B - (constrain(mv, MV_LO, MV_HI) - MV_LO) * h / (MV_HI - MV_LO);
}
static int yMv(const lv_area_t& a, int y) {
const int h = lv_area_get_height(&a) - PAD_T - PAD_B;
return MV_LO + (a.y2 - PAD_B - y) * (MV_HI - MV_LO) / h;
}
static void text(lv_layer_t* layer, const char* t, int x, int y, int w, lv_text_align_t align, 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 = align;
d.text = t;
d.text_local = 1;
lv_area_t r = { x, y, x + w - 1, y + 16 };
lv_draw_label(layer, &d, &r);
}
static void line(lv_layer_t* layer, int x1, int y1, int x2, int y2, uint32_t col, int w, int dash = 0) {
lv_draw_line_dsc_t d;
lv_draw_line_dsc_init(&d);
d.color = lv_color_hex(col);
d.width = w;
d.dash_width = dash;
d.dash_gap = dash;
d.p1.x = x1; d.p1.y = y1; d.p2.x = x2; d.p2.y = y2;
lv_draw_line(layer, &d);
}
static void onDraw(lv_event_t* e) {
lv_obj_t* obj = lv_event_get_current_target_obj(e);
lv_layer_t* layer = lv_event_get_layer(e);
lv_area_t a;
lv_obj_get_coords(obj, &a);
char t[16];
for (int mv = 3200; mv <= MV_HI; mv += 400) { // voltage grid
const int y = mvY(a, mv);
line(layer, a.x1 + PAD_L, y, a.x2 - PAD_R, y, theme::SURFACE_2, 1);
snprintf(t, sizeof(t), "%d.%d V", mv / 1000, mv % 1000 / 100);
text(layer, t, a.x1, y - 8, PAD_L - 6, LV_TEXT_ALIGN_RIGHT, theme::TEXT_MUTED);
}
for (int i = 0; i < PTS; i += 5) { // 0, 50, 100 %
snprintf(t, sizeof(t), "%d%%", i * 10);
const int x = i == PTS - 1 ? a.x2 - 40 : ptX(a, i) - 20; // the last one ends at the edge
text(layer, t, x, a.y2 - PAD_B + 4, 40, i == PTS - 1 ? LV_TEXT_ALIGN_RIGHT : LV_TEXT_ALIGN_CENTER, theme::TEXT_MUTED);
}
const int now = s_ui ? s_ui->battMv() : 0;
if (now > 0) line(layer, a.x1 + PAD_L, mvY(a, now), a.x2 - PAD_R, mvY(a, now), theme::OK, 1, 3);
for (int i = 1; i < PTS; i++)
line(layer, ptX(a, i - 1), mvY(a, battery::curveMvAt(i - 1)), ptX(a, i), mvY(a, battery::curveMvAt(i)), theme::ACCENT, 2);
for (int i = 0; i < PTS; i++) {
lv_draw_rect_dsc_t r;
lv_draw_rect_dsc_init(&r);
r.radius = LV_RADIUS_CIRCLE;
r.bg_color = lv_color_hex(i == s_drag ? theme::ACCENT : theme::BG);
r.border_color = lv_color_hex(theme::ACCENT);
r.border_width = 2;
const int d = i == s_drag ? 7 : 4, x = ptX(a, i), y = mvY(a, battery::curveMvAt(i));
lv_area_t c = { x - d, y - d, x + d, y + d };
lv_draw_rect(layer, &r, &c);
}
if (s_drag >= 0) { // the point being moved, above it
const int x = ptX(a, s_drag), y = mvY(a, battery::curveMvAt(s_drag));
snprintf(t, sizeof(t), "%d%% %d mV", s_drag * 10, battery::curveMvAt(s_drag));
const int lx = constrain(x - 50, (int)a.x1, (int)a.x2 - 100);
lv_draw_rect_dsc_t bg; // on a pill, clear of the grid and the "now" line
lv_draw_rect_dsc_init(&bg);
bg.radius = theme::RADIUS_SM;
bg.bg_color = lv_color_hex(theme::SURFACE_2);
lv_area_t ba = { lx, y - 30, lx + 99, y - 12 };
lv_draw_rect(layer, &bg, &ba);
text(layer, t, lx, y - 29, 100, LV_TEXT_ALIGN_CENTER, theme::TEXT);
}
}
static void onTouch(lv_event_t* e) {
lv_event_code_t code = lv_event_get_code(e);
lv_obj_t* obj = lv_event_get_current_target_obj(e);
lv_indev_t* in = lv_indev_active();
lv_point_t p = { 0, 0 };
if (in) lv_indev_get_point(in, &p);
lv_area_t a;
lv_obj_get_coords(obj, &a);
if (code == LV_EVENT_PRESSED) { // the point nearest along the % axis
int best = 0;
for (int i = 1; i < PTS; i++) if (abs(ptX(a, i) - p.x) < abs(ptX(a, best) - p.x)) best = i;
s_drag = best;
}
NodePrefs* np = s_ui->prefsMut();
if ((code == LV_EVENT_PRESSED || code == LV_EVENT_PRESSING) && s_drag >= 0 && np) {
uint16_t* c = np->batt_curve_mv;
if (!battery::validCurve(c))
for (int k = 0; k < PTS; k++) c[k] = (uint16_t)battery::builtinMvAt(k);
const int lo = s_drag > 0 ? c[s_drag - 1] + 10 : battery::CURVE_MIN_MV;
const int hi = s_drag < PTS - 1 ? c[s_drag + 1] - 10 : battery::CURVE_MAX_MV;
const int mv = constrain((yMv(a, p.y) + 5) / 10 * 10, lo, hi);
if (mv != c[s_drag]) { c[s_drag] = (uint16_t)mv; lv_obj_invalidate(obj); s_ui->refreshBattCurve(); }
}
if (code == LV_EVENT_RELEASED || code == LV_EVENT_PRESS_LOST) {
s_drag = -1;
lv_obj_invalidate(obj);
prefsSave();
}
}
static void onReset(lv_event_t* e) {
(void)e;
NodePrefs* np = s_ui->prefsMut();
if (!np || !battery::validCurve(np->batt_curve_mv)) return;
memset(np->batt_curve_mv, 0, sizeof(np->batt_curve_mv));
prefsSave();
if (s_chart) lv_obj_invalidate(s_chart);
s_ui->refreshBattCurve();
}
} // namespace battview
void UITask::showBattCurve() {
using namespace battview;
_screen = SCR_BATT;
lv_obj_t* body = newScreen("Battery curve", true);
headerButton(_header, LV_SYMBOL_REFRESH " Reset", onReset, 8, &s_reset);
lv_obj_remove_flag(body, LV_OBJ_FLAG_SCROLLABLE);
s_chart = lv_obj_create(body);
styleSurface(s_chart, theme::BG);
lv_obj_remove_flag(s_chart, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_set_width(s_chart, LV_PCT(100));
lv_obj_set_flex_grow(s_chart, 1);
lv_obj_add_flag(s_chart, LV_OBJ_FLAG_CLICKABLE);
lv_obj_add_flag(s_chart, LV_OBJ_FLAG_PRESS_LOCK); // still its drag when the finger strays off it
lv_obj_add_event_cb(s_chart, onDraw, LV_EVENT_DRAW_MAIN_END, NULL);
lv_obj_add_event_cb(s_chart, onTouch, LV_EVENT_ALL, NULL);
s_now = noteLabel(body, "");
s_drag = -1;
refreshBattCurve();
}
// Once a second from loop(), and after a change: the cell now, the reset's state.
void UITask::refreshBattCurve() {
using namespace battview;
if (_screen != SCR_BATT || !s_now) return;
const int mv = battMv();
const bool custom = _prefs && battery::validCurve(_prefs->batt_curve_mv);
char t[64];
if (mv > 0) snprintf(t, sizeof(t), "Now %d mV - %d %% - %s curve", mv, battery::rawPercent(mv), custom ? "your" : "LiPo");
else snprintf(t, sizeof(t), "%s curve - drag a point up or down", custom ? "Your" : "LiPo");
lv_label_set_text(s_now, t);
if (s_reset) lv_obj_set_style_text_opa(s_reset, custom ? LV_OPA_COVER : LV_OPA_40, 0);
if (s_chart && s_drag < 0) lv_obj_invalidate(s_chart); // the "now" line moves with the cell
}