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>
This commit is contained in:
Jakub
2026-10-02 11:25:58 +02:00
co-authored by Claude Opus 5.5
parent 1166ea8dde
commit 61c75946f6
21 changed files with 702 additions and 227 deletions
+59 -10
View File
@@ -153,7 +153,8 @@ static mapview::Grid s_grid; // under the tiles, as on the map
static void onTap(lv_event_t* e) { (void)e; s_ui->openMap(true); }
static lv_obj_t* dot(uint8_t kind) {
static const struct { uint8_t d; uint32_t col, border; } LOOK[] = {
// Not static: the accent can change after the first dot (Settings > Display).
const struct { uint8_t d; uint32_t col, border; } LOOK[] = {
{ 14, theme::ACCENT, theme::BG }, { 14, theme::BG, theme::ACCENT }, // you; no fix: the last position, hollow
{ 12, theme::BG, theme::ACCENT }, { 12, theme::OK, theme::BG }, // the target; live shares
};
@@ -199,7 +200,9 @@ static lv_obj_t* homeTile(lv_obj_t* parent, const char* icon, const char* text,
// Every app is on the pages, a hidden one dimmed with a "+" (the others a
// "-"). A tap hides / shows it; a drag lifts a copy of the tile on the top
// layer (the tile dims in place) and drops the app into the slot under the
// finger; held at a screen edge, it goes to that side's page. Done, or the
// finger. While it's dragged the page already shows where it would land: the
// dimmed tile takes that slot and the tiles it pushes glide, names and all, to
// theirs. Held at a screen edge, it goes to that side's page. Done, or the
// side button, ends it.
namespace home {
static lv_obj_t* s_ghost = nullptr;
@@ -208,12 +211,17 @@ static bool s_dragged = false; // past the slop: the release is a drop, no
static lv_point_t s_drag_start, s_ghost_org;
static uint32_t s_edge_since = 0;
static const int DRAG_SLOP = 8, EDGE_PX = 20;
static const uint32_t EDGE_MS = 600;
static const uint32_t EDGE_MS = 600, GLIDE_MS = 160;
static lv_point_t s_slot_c[PER_PAGE]; // the slots' centres as the page was laid out
static int s_slots = 0;
static int s_preview = -1; // the slot the dragged tile shows in now
static void dragEnd() {
if (s_ghost) { lv_obj_delete(s_ghost); s_ghost = nullptr; }
s_drag_pos = -1;
s_edge_since = 0;
s_slots = 0;
s_preview = -1;
}
// Leaving Home: arranging ends and the order is saved (UITask::newScreen).
static void leave() {
@@ -222,19 +230,56 @@ static void leave() {
flushOrder();
}
// The tile on this page nearest the point, as a slot 0..PER_PAGE-1.
// Where the slots are, taken once when a drag starts: the tiles then move
// about under the finger, the slots don't.
static void captureSlots() {
s_slots = 0;
for (int i = 0; i < (int)lv_obj_get_child_count(s_page_box) && i < PER_PAGE; i++) {
lv_area_t a;
lv_obj_get_coords(lv_obj_get_child(s_page_box, i), &a);
s_slot_c[s_slots++] = { (a.x1 + a.x2) / 2, (a.y1 + a.y2) / 2 };
}
}
// The slot on this page nearest the point, 0..PER_PAGE-1.
static int slotAt(lv_point_t p) {
int best = 0;
int32_t bd = INT32_MAX;
for (int i = 0; i < (int)lv_obj_get_child_count(s_page_box); i++) {
lv_area_t a;
lv_obj_get_coords(lv_obj_get_child(s_page_box, i), &a);
int32_t dx = p.x - (a.x1 + a.x2) / 2, dy = p.y - (a.y1 + a.y2) / 2;
for (int i = 0; i < s_slots; i++) {
int32_t dx = p.x - s_slot_c[i].x, dy = p.y - s_slot_c[i].y;
if (dx * dx + dy * dy < bd) { bd = dx * dx + dy * dy; best = i; }
}
return best;
}
// The dragged tile `t` into `slot`, the others closing up round it: each
// tile is laid out at its new place, then glides there from where it showed.
static void previewSlot(lv_obj_t* t, int slot) {
if (slot == s_preview) return;
s_preview = slot;
const int n = (int)lv_obj_get_child_count(s_page_box);
lv_obj_t* objs[PER_PAGE];
lv_point_t was[PER_PAGE];
for (int i = 0; i < n && i < PER_PAGE; i++) {
objs[i] = lv_obj_get_child(s_page_box, i);
lv_area_t a;
lv_obj_get_coords(objs[i], &a); // where it shows, mid-glide included
was[i] = { a.x1, a.y1 };
lv_anim_delete(objs[i], anim::setTx);
lv_anim_delete(objs[i], anim::setTy);
lv_obj_set_style_translate_x(objs[i], 0, 0);
lv_obj_set_style_translate_y(objs[i], 0, 0);
}
lv_obj_move_to_index(t, slot);
lv_obj_update_layout(s_page_box);
for (int i = 0; i < n && i < PER_PAGE; i++) {
lv_area_t a;
lv_obj_get_coords(objs[i], &a);
if (was[i].x != a.x1) anim::run(objs[i], anim::setTx, was[i].x - a.x1, 0, GLIDE_MS);
if (was[i].y != a.y1) anim::run(objs[i], anim::setTy, was[i].y - a.y1, 0, GLIDE_MS);
}
}
// The page is rebuilt after the event that changed it, not inside it.
static int s_pending_from, s_pending_to, s_pending_page;
static void applyDrop(void*) { s_ui->homeAppDrop(s_pending_from, s_pending_to); if (s_pending_page >= 0) s_ui->setHomePage(s_pending_page); }
@@ -274,10 +319,13 @@ static void onEditTile(lv_event_t* e) {
lv_obj_get_coords(t, &c);
s_ghost_org = { c.x1, c.y1 };
lv_obj_set_style_opa(t, LV_OPA_20, 0);
captureSlots();
s_preview = lv_obj_get_index(t);
}
lv_obj_set_pos(s_ghost, s_ghost_org.x + p.x - s_drag_start.x, s_ghost_org.y + p.y - s_drag_start.y);
int w = lv_display_get_horizontal_resolution(NULL);
int side = p.x < EDGE_PX ? -1 : p.x >= w - EDGE_PX ? 1 : 0;
if (!side) previewSlot(t, slotAt(p));
int page = s_page + side;
if (!side || page < APPS || page >= pageCount()) { s_edge_since = 0; return; }
if (!s_edge_since) { s_edge_since = millis() | 1; return; }
@@ -290,10 +338,11 @@ static void onEditTile(lv_event_t* e) {
return;
}
bool dropped = s_dragged && s_ghost;
int slot = s_preview >= 0 ? s_preview : slotAt(p); // where the page already shows it
dragEnd();
if (code == LV_EVENT_PRESS_LOST) { lv_obj_set_style_opa(t, LV_OPA_COVER, 0); return; }
if (code == LV_EVENT_PRESS_LOST) { later(pos, pos, -1, false); return; } // rebuilt as it was
if (!dropped) { later(pos, 0, -1, true); return; } // a tap
int to = (s_page - APPS) * PER_PAGE + slotAt(p);
int to = (s_page - APPS) * PER_PAGE + slot;
later(pos, to < APP_COUNT ? to : APP_COUNT - 1, -1, false);
}