mirror of
https://github.com/MarekZegare4/MeshCore-Solo.git
synced 2026-11-01 10:21:59 +00:00
feat(l2): long trails with a card copy; map follow and redraw fixes
Trail on the L2: 32768 points every 5 m, no simplification, no point spacing setting; copied to the card every minute and restored at power-on. The line keeps up with the position dot (incremental sync, pending point). Map: a tap's finger jitter no longer turns follow off; the GPS button shows when the map follows. The trail is drawn as one polyline with breaks and off-view chunks skipped (an lv_line queued a draw task per point). Fewer rebuilds: conversation bubbles patched (new messages, delivery marks), home mini-map dots reused, Nearby rows rewritten in place, history read in runs and each file opened once. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -28,6 +28,9 @@ per-user. Listed here so the rest of this page can assume them.
|
|||||||
| `MAX_CONTACTS=<n>` | Size of the contact table. Default is 32 if unset; solo builds set 350. |
|
| `MAX_CONTACTS=<n>` | Size of the contact table. Default is 32 if unset; solo builds set 350. |
|
||||||
| `MAX_GROUP_CHANNELS=<n>` | Size of the channel list. Required for channel support to compile in at all — not optional the way the rest of this page is. |
|
| `MAX_GROUP_CHANNELS=<n>` | Size of the channel list. Required for channel support to compile in at all — not optional the way the rest of this page is. |
|
||||||
| `OFFLINE_QUEUE_SIZE=<n>` | How many messages queue for later delivery while the phone app is disconnected. Default 16; solo builds set 256. |
|
| `OFFLINE_QUEUE_SIZE=<n>` | How many messages queue for later delivery while the phone app is disconnected. Default 16; solo builds set 256. |
|
||||||
|
| `TRAIL_CAPACITY=<n>` | Points the GPS trail holds (16 B each). Default 512 for the nRF52's RAM; the L2 sets 32768 in PSRAM. At most 65535 (the save format's count). |
|
||||||
|
| `TRAIL_SIMPLIFY=0` | Keeps every trail sample instead of storing straight stretches as their two ends. For boards with the memory for it (the L2). |
|
||||||
|
| `TRAIL_FIXED_MIN_DELTA_M=<m>` | A fixed distance between trail points instead of the Point spacing setting, which is then hidden (the L2: 5 m, against GPS jitter). |
|
||||||
| `UI_SENSORS_PAGE=1` | Enables the on-device sensors dashboard page. |
|
| `UI_SENSORS_PAGE=1` | Enables the on-device sensors dashboard page. |
|
||||||
| `BLE_PIN_CODE=<n>` | See below — not a plain fixed value in practice. |
|
| `BLE_PIN_CODE=<n>` | See below — not a plain fixed value in practice. |
|
||||||
| `DISPLAY_CLASS=<Class>` | Selects the display driver (e.g. `SSD1306Display`, `GxEPDDisplay`, `ST7789Display` — see `src/helpers/ui/` for the full set). Fixed by whatever panel the board actually has; only relevant if you're wiring on a *different* display than stock, in which case the matching driver's `.cpp` also needs adding to `build_src_filter`. |
|
| `DISPLAY_CLASS=<Class>` | Selects the display driver (e.g. `SSD1306Display`, `GxEPDDisplay`, `ST7789Display` — see `src/helpers/ui/` for the full set). Fixed by whatever panel the board actually has; only relevant if you're wiring on a *different* display than stock, in which case the matching driver's `.cpp` also needs adding to `build_src_filter`. |
|
||||||
|
|||||||
@@ -36,7 +36,11 @@ Chrome or Edge and click **Connect device**, then on the device choose
|
|||||||
it's connected over USB.
|
it's connected over USB.
|
||||||
|
|
||||||
> [!NOTE]
|
> [!NOTE]
|
||||||
> **Wio Tracker L2:** the trail holds 4096 points and is drawn on the map.
|
> **Wio Tracker L2:** the trail holds 32,768 points, every GPS fix 5 m or
|
||||||
|
> more from the last one (no simplification and no Min dist setting), about
|
||||||
|
> 160 km on foot. A copy on the SD card is updated every minute, so a restart
|
||||||
|
> or a flat battery doesn't lose it: the trail comes back at power-on, still
|
||||||
|
> recording if it was. It is drawn on the map and keeps up with your position.
|
||||||
> Its controls (start / stop, save, load, track back, reset) are in the map's
|
> Its controls (start / stop, save, load, track back, reset) are in the map's
|
||||||
> **Map tools**, the settings in Settings › Map. **GPX** writes the trail to
|
> **Map tools**, the settings in Settings › Map. **GPX** writes the trail to
|
||||||
> `trails/` on the SD card; take it off with the card as a USB drive
|
> `trails/` on the SD card; take it off with the card as a USB drive
|
||||||
|
|||||||
@@ -23,11 +23,17 @@
|
|||||||
// turn still gets a vertex roughly every min-delta of deviation — so CAPACITY
|
// turn still gets a vertex roughly every min-delta of deviation — so CAPACITY
|
||||||
// covers a much longer route than CAPACITY × min-delta would suggest.
|
// covers a much longer route than CAPACITY × min-delta would suggest.
|
||||||
|
|
||||||
// Boards with PSRAM raise it (the L2: -D TRAIL_CAPACITY=4096, 64 KB, since
|
// Boards with PSRAM raise it (the L2: -D TRAIL_CAPACITY=32768, 512 KB, since
|
||||||
// UiCore is heap-allocated there, in PSRAM); the default stays for the nRF52.
|
// UiCore is heap-allocated there, in PSRAM); the default stays for the nRF52.
|
||||||
#ifndef TRAIL_CAPACITY
|
#ifndef TRAIL_CAPACITY
|
||||||
#define TRAIL_CAPACITY 512
|
#define TRAIL_CAPACITY 512
|
||||||
#endif
|
#endif
|
||||||
|
// Straight-run simplification (addPoint()): on by default, for the nRF52's
|
||||||
|
// 512 points. A board with room for every sample turns it off
|
||||||
|
// (-D TRAIL_SIMPLIFY=0): each sample past the min-delta gate is a vertex.
|
||||||
|
#ifndef TRAIL_SIMPLIFY
|
||||||
|
#define TRAIL_SIMPLIFY 1
|
||||||
|
#endif
|
||||||
|
|
||||||
struct TrailPoint {
|
struct TrailPoint {
|
||||||
int32_t lat_1e6;
|
int32_t lat_1e6;
|
||||||
@@ -140,8 +146,19 @@ public:
|
|||||||
_session_start_ms = 0;
|
_session_start_ms = 0;
|
||||||
_paused = false;
|
_paused = false;
|
||||||
_has_pending = false;
|
_has_pending = false;
|
||||||
|
_dist_m = 0;
|
||||||
|
_gen++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// For whoever mirrors the trail (the map's line, the L2's card copy): gen()
|
||||||
|
// changes when the points are replaced (clear, load), seq() counts every
|
||||||
|
// point ever added -- the newest seq() - old_seq points are new, unless the
|
||||||
|
// ring has also dropped its oldest (count() == CAPACITY).
|
||||||
|
uint32_t gen() const { return _gen; }
|
||||||
|
uint32_t seq() const { return _seq; }
|
||||||
|
// The simplifier's uncommitted candidate: where the trail really ends now.
|
||||||
|
bool pendingPoint(TrailPoint& out) const { if (_has_pending) out = _pending; return _has_pending; }
|
||||||
|
|
||||||
// Returns true if the sample was accepted (passed the min-delta gate) —
|
// Returns true if the sample was accepted (passed the min-delta gate) —
|
||||||
// whether that landed it as a new committed vertex right away, or just
|
// whether that landed it as a new committed vertex right away, or just
|
||||||
// extended the pending candidate (see below). First point of the ring and
|
// extended the pending candidate (see below). First point of the ring and
|
||||||
@@ -192,6 +209,10 @@ public:
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if !TRAIL_SIMPLIFY
|
||||||
|
commitPoint(lat_1e6, lon_1e6, ts, 0);
|
||||||
|
return true;
|
||||||
|
#endif
|
||||||
TrailPoint sample{ lat_1e6, lon_1e6, ts, 0 };
|
TrailPoint sample{ lat_1e6, lon_1e6, ts, 0 };
|
||||||
if (!_has_pending) {
|
if (!_has_pending) {
|
||||||
_pending = sample; // last in-corridor sample (the commit candidate)
|
_pending = sample; // last in-corridor sample (the commit candidate)
|
||||||
@@ -215,13 +236,10 @@ public:
|
|||||||
// uncommitted candidate (_pending) is counted too, so the live total doesn't
|
// uncommitted candidate (_pending) is counted too, so the live total doesn't
|
||||||
// stall over a long straight run — where simplification holds the whole
|
// stall over a long straight run — where simplification holds the whole
|
||||||
// stretch as one pending point until a bend forces a commit (see addPoint()).
|
// stretch as one pending point until a bend forces a commit (see addPoint()).
|
||||||
|
// Kept as a running total (commitPoint()), not summed on every call: the
|
||||||
|
// L2's trail runs to tens of thousands of points.
|
||||||
uint32_t totalDistanceMeters() const {
|
uint32_t totalDistanceMeters() const {
|
||||||
float d = 0;
|
float d = _dist_m;
|
||||||
for (int i = 1; i < _count; i++) {
|
|
||||||
if (at(i).flags & TRAIL_FLAG_SEG_START) continue;
|
|
||||||
d += haversineMeters(at(i - 1).lat_1e6, at(i - 1).lon_1e6,
|
|
||||||
at(i).lat_1e6, at(i).lon_1e6);
|
|
||||||
}
|
|
||||||
if (_has_pending && _count > 0)
|
if (_has_pending && _count > 0)
|
||||||
d += haversineMeters(last().lat_1e6, last().lon_1e6,
|
d += haversineMeters(last().lat_1e6, last().lon_1e6,
|
||||||
_pending.lat_1e6, _pending.lon_1e6);
|
_pending.lat_1e6, _pending.lon_1e6);
|
||||||
@@ -305,6 +323,11 @@ public:
|
|||||||
}
|
}
|
||||||
_accumulated_ms = accum;
|
_accumulated_ms = accum;
|
||||||
_pending_seg_break = true;
|
_pending_seg_break = true;
|
||||||
|
_dist_m = 0;
|
||||||
|
for (int i = 1; i < _count; i++)
|
||||||
|
if (!(_buf[i].flags & TRAIL_FLAG_SEG_START))
|
||||||
|
_dist_m += haversineMeters(_buf[i - 1].lat_1e6, _buf[i - 1].lon_1e6, _buf[i].lat_1e6, _buf[i].lon_1e6);
|
||||||
|
_gen++;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -464,6 +487,9 @@ private:
|
|||||||
bool _pending_seg_break = false; // next addPoint flags itself SEG_START
|
bool _pending_seg_break = false; // next addPoint flags itself SEG_START
|
||||||
uint32_t _accumulated_ms = 0; // banked active time across previous sessions
|
uint32_t _accumulated_ms = 0; // banked active time across previous sessions
|
||||||
uint32_t _session_start_ms = 0; // millis() of the current active session, 0 if none
|
uint32_t _session_start_ms = 0; // millis() of the current active session, 0 if none
|
||||||
|
float _dist_m = 0; // totalDistanceMeters() without the pending candidate
|
||||||
|
uint32_t _gen = 0; // see gen() / seq()
|
||||||
|
uint32_t _seq = 0;
|
||||||
|
|
||||||
// Streaming simplification: a sample that passed the min-delta gate but
|
// Streaming simplification: a sample that passed the min-delta gate but
|
||||||
// hasn't been committed as a real vertex yet — see addPoint(). _dir is the
|
// hasn't been committed as a real vertex yet — see addPoint(). _dir is the
|
||||||
@@ -474,14 +500,20 @@ private:
|
|||||||
TrailPoint _dir;
|
TrailPoint _dir;
|
||||||
|
|
||||||
void commitPoint(int32_t lat_1e6, int32_t lon_1e6, uint32_t ts, uint8_t flags) {
|
void commitPoint(int32_t lat_1e6, int32_t lon_1e6, uint32_t ts, uint8_t flags) {
|
||||||
|
if (_count > 0 && !(flags & TRAIL_FLAG_SEG_START))
|
||||||
|
_dist_m += haversineMeters(last().lat_1e6, last().lon_1e6, lat_1e6, lon_1e6);
|
||||||
int pos;
|
int pos;
|
||||||
if (_count < CAPACITY) {
|
if (_count < CAPACITY) {
|
||||||
pos = (_head + _count) % CAPACITY;
|
pos = (_head + _count) % CAPACITY;
|
||||||
_count++;
|
_count++;
|
||||||
} else {
|
} else { // full: the oldest point goes, and the stretch from it to the next
|
||||||
|
if (!(at(1).flags & TRAIL_FLAG_SEG_START))
|
||||||
|
_dist_m -= haversineMeters(at(0).lat_1e6, at(0).lon_1e6, at(1).lat_1e6, at(1).lon_1e6);
|
||||||
|
if (_dist_m < 0) _dist_m = 0;
|
||||||
pos = _head;
|
pos = _head;
|
||||||
_head = (_head + 1) % CAPACITY;
|
_head = (_head + 1) % CAPACITY;
|
||||||
}
|
}
|
||||||
|
_seq++;
|
||||||
_buf[pos].lat_1e6 = lat_1e6;
|
_buf[pos].lat_1e6 = lat_1e6;
|
||||||
_buf[pos].lon_1e6 = lon_1e6;
|
_buf[pos].lon_1e6 = lon_1e6;
|
||||||
_buf[pos].ts = ts;
|
_buf[pos].ts = ts;
|
||||||
|
|||||||
@@ -61,9 +61,11 @@ struct Setting {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// ── Value labels ──────────────────────────────────────────────────────────────
|
// ── Value labels ──────────────────────────────────────────────────────────────
|
||||||
|
#ifndef TRAIL_FIXED_MIN_DELTA_M
|
||||||
static void optMinDelta(uint8_t v, char* b, int n, const NodePrefs& p) {
|
static void optMinDelta(uint8_t v, char* b, int n, const NodePrefs& p) {
|
||||||
snprintf(b, n, "%s", TrailStore::minDeltaLabel(v, p.units_imperial));
|
snprintf(b, n, "%s", TrailStore::minDeltaLabel(v, p.units_imperial));
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
static void optAutoPause(uint8_t v, char* b, int n, const NodePrefs&) {
|
static void optAutoPause(uint8_t v, char* b, int n, const NodePrefs&) {
|
||||||
uint16_t s = NodePrefs::trailAutoPauseSecs(v);
|
uint16_t s = NodePrefs::trailAutoPauseSecs(v);
|
||||||
if (s == 0) snprintf(b, n, "Off"); else snprintf(b, n, "%u min", (unsigned)(s / 60));
|
if (s == 0) snprintf(b, n, "Off"); else snprintf(b, n, "%u min", (unsigned)(s / 60));
|
||||||
@@ -160,8 +162,10 @@ static void applySound(UiCore& c) { c.host()->applySoundPrefs(); }
|
|||||||
#define SW(label, hint, sec, f, chg) { label, hint, sec, NP_OFF(f), 2, nullptr, chg, nullptr, 1 }
|
#define SW(label, hint, sec, f, chg) { label, hint, sec, NP_OFF(f), 2, nullptr, chg, nullptr, 1 }
|
||||||
#define MAP(label, hint, sec, f, vals, opt, chg) { label, hint, sec, NP_OFF(f), COUNT_OF(vals), opt, chg, vals, NP_SIZE(f) }
|
#define MAP(label, hint, sec, f, vals, opt, chg) { label, hint, sec, NP_OFF(f), COUNT_OF(vals), opt, chg, vals, NP_SIZE(f) }
|
||||||
static const Setting ALL[] = {
|
static const Setting ALL[] = {
|
||||||
|
#ifndef TRAIL_FIXED_MIN_DELTA_M // else every sample past a fixed jitter gate is kept
|
||||||
IDX("Point spacing", "Between trail points", SEC_TRAIL, trail_min_delta_idx,
|
IDX("Point spacing", "Between trail points", SEC_TRAIL, trail_min_delta_idx,
|
||||||
TrailStore::MIN_DELTA_COUNT, optMinDelta, nullptr),
|
TrailStore::MIN_DELTA_COUNT, optMinDelta, nullptr),
|
||||||
|
#endif
|
||||||
IDX("Auto-pause", "Pause when standing still", SEC_TRAIL, trail_autopause_idx,
|
IDX("Auto-pause", "Pause when standing still", SEC_TRAIL, trail_autopause_idx,
|
||||||
NodePrefs::TRAIL_AUTOPAUSE_COUNT, optAutoPause, nullptr),
|
NodePrefs::TRAIL_AUTOPAUSE_COUNT, optAutoPause, nullptr),
|
||||||
SW("Save on low battery", "Save it on low battery", SEC_TRAIL, trail_autosave_lowbatt, nullptr),
|
SW("Save on low battery", "Save it on low battery", SEC_TRAIL, trail_autosave_lowbatt, nullptr),
|
||||||
|
|||||||
@@ -21,7 +21,11 @@ public:
|
|||||||
_next_sample_ms = millis() + (uint32_t)TrailStore::SAMPLING_SECS * 1000UL;
|
_next_sample_ms = millis() + (uint32_t)TrailStore::SAMPLING_SECS * 1000UL;
|
||||||
int32_t la, lo;
|
int32_t la, lo;
|
||||||
if (!_course->currentLocation(la, lo)) return;
|
if (!_course->currentLocation(la, lo)) return;
|
||||||
|
#ifdef TRAIL_FIXED_MIN_DELTA_M // no point spacing setting: a fixed GPS-jitter gate (the L2)
|
||||||
|
uint16_t md = TRAIL_FIXED_MIN_DELTA_M;
|
||||||
|
#else
|
||||||
uint16_t md = TrailStore::minDeltaMeters(_prefs->trail_min_delta_idx, _prefs->units_imperial);
|
uint16_t md = TrailStore::minDeltaMeters(_prefs->trail_min_delta_idx, _prefs->units_imperial);
|
||||||
|
#endif
|
||||||
// Auto-pause: freeze the trail once the device has stayed within
|
// Auto-pause: freeze the trail once the device has stayed within
|
||||||
// TRAIL_AUTOPAUSE_MOVE_M of one spot for the configured delay; resume on
|
// TRAIL_AUTOPAUSE_MOVE_M of one spot for the configured delay; resume on
|
||||||
// the next real move. Its own coarse gate (not the trail min-delta) so
|
// the next real move. Its own coarse gate (not the trail min-delta) so
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ static void onRoomRowHold(lv_event_t* e) {
|
|||||||
static void onConvThreadMenu(lv_event_t* e) { (void)e; s_ui->conversationMenu(nullptr); }
|
static void onConvThreadMenu(lv_event_t* e) { (void)e; s_ui->conversationMenu(nullptr); }
|
||||||
static void onMsgHold(lv_event_t* e) {
|
static void onMsgHold(lv_event_t* e) {
|
||||||
lv_indev_wait_release(lv_indev_active());
|
lv_indev_wait_release(lv_indev_active());
|
||||||
s_ui->messageMenu((int)(uintptr_t)lv_event_get_user_data(e));
|
s_ui->messageMenu(threadRowOf((lv_obj_t*)lv_event_get_current_target(e)));
|
||||||
}
|
}
|
||||||
static void onChatFilter(lv_event_t* e) { s_ui->toggleChatFilter((uint8_t)(uintptr_t)lv_event_get_user_data(e)); }
|
static void onChatFilter(lv_event_t* e) { s_ui->toggleChatFilter((uint8_t)(uintptr_t)lv_event_get_user_data(e)); }
|
||||||
|
|
||||||
|
|||||||
@@ -121,22 +121,21 @@ public:
|
|||||||
}
|
}
|
||||||
|
|
||||||
// A window of the conversation, oldest first: `n` records ending `skip`
|
// A window of the conversation, oldest first: `n` records ending `skip`
|
||||||
// before the newest. Returns how many were read.
|
// before the newest. Returns how many were read; *total = records in the
|
||||||
|
// file (0 when missing or unreadable).
|
||||||
template <typename E>
|
template <typename E>
|
||||||
int window(const char* path, int skip, int n, E* out) {
|
int window(const char* path, int skip, int n, E* out, int* total) {
|
||||||
|
*total = 0;
|
||||||
FILE* f = fopen(path, "rb");
|
FILE* f = fopen(path, "rb");
|
||||||
if (!f) return 0;
|
if (!f) return 0;
|
||||||
Hdr h;
|
Hdr h;
|
||||||
int got = 0;
|
int got = 0;
|
||||||
if (readHdr<E>(f, h) && skip < (int)h.count) {
|
if (readHdr<E>(f, h)) {
|
||||||
int avail = (int)h.count - skip;
|
*total = (int)h.count;
|
||||||
int m = n < avail ? n : avail;
|
if (skip < (int)h.count) {
|
||||||
uint32_t k0 = h.count - skip - m; // index from the oldest
|
int avail = (int)h.count - skip;
|
||||||
for (int i = 0; i < m; i++) {
|
int m = n < avail ? n : avail;
|
||||||
uint32_t slot = (h.head + k0 + i) % h.cap;
|
got = (int)readRun(f, h, h.count - skip - m, (uint32_t)m, out);
|
||||||
if (fseek(f, sizeof(Hdr) + (long)slot * sizeof(E), SEEK_SET) != 0) break;
|
|
||||||
if (fread(&out[got], sizeof(E), 1, f) != 1) break;
|
|
||||||
got++;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fclose(f);
|
fclose(f);
|
||||||
@@ -184,15 +183,34 @@ private:
|
|||||||
if (fseek(f, 0, SEEK_SET) != 0 || fread(&h, sizeof(h), 1, f) != 1) return false;
|
if (fseek(f, 0, SEEK_SET) != 0 || fread(&h, sizeof(h), 1, f) != 1) return false;
|
||||||
return h.magic == MAGIC && h.ver == VERSION && h.rec_size == sizeof(E) && h.cap > 0 && h.count <= h.cap;
|
return h.magic == MAGIC && h.ver == VERSION && h.rec_size == sizeof(E) && h.cap > 0 && h.count <= h.cap;
|
||||||
}
|
}
|
||||||
|
// `m` records from the k0-th oldest into `out`: one read, two where the
|
||||||
|
// ring wraps. Returns how many were read.
|
||||||
|
template <typename E>
|
||||||
|
static uint32_t readRun(FILE* f, const Hdr& h, uint32_t k0, uint32_t m, E* out) {
|
||||||
|
uint32_t got = 0;
|
||||||
|
while (got < m) {
|
||||||
|
uint32_t slot = (h.head + k0 + got) % h.cap;
|
||||||
|
uint32_t run = m - got < h.cap - slot ? m - got : h.cap - slot;
|
||||||
|
if (fseek(f, sizeof(Hdr) + (long)slot * sizeof(E), SEEK_SET) != 0) break;
|
||||||
|
size_t r = fread(&out[got], sizeof(E), run, f);
|
||||||
|
got += (uint32_t)r;
|
||||||
|
if (r != run) break;
|
||||||
|
}
|
||||||
|
return got;
|
||||||
|
}
|
||||||
static bool writeHdr(FILE* f, const Hdr& h) {
|
static bool writeHdr(FILE* f, const Hdr& h) {
|
||||||
return fseek(f, 0, SEEK_SET) == 0 && fwrite(&h, sizeof(h), 1, f) == 1;
|
return fseek(f, 0, SEEK_SET) == 0 && fwrite(&h, sizeof(h), 1, f) == 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
template <typename E>
|
template <typename E>
|
||||||
void append(const char* path, E& e) {
|
void append(const char* path, E& e) {
|
||||||
resize<E>(path, _keep); // the setting changed since this file was written
|
|
||||||
FILE* f = fopen(path, "r+b");
|
FILE* f = fopen(path, "r+b");
|
||||||
Hdr h;
|
Hdr h;
|
||||||
|
if (f && readHdr<E>(f, h) && h.cap != _keep) { // the setting changed since this file was written
|
||||||
|
fclose(f);
|
||||||
|
resize<E>(path, _keep);
|
||||||
|
f = fopen(path, "r+b");
|
||||||
|
}
|
||||||
if (!f || !readHdr<E>(f, h)) { // new (or unreadable: start it again)
|
if (!f || !readHdr<E>(f, h)) { // new (or unreadable: start it again)
|
||||||
if (f) fclose(f);
|
if (f) fclose(f);
|
||||||
f = fopen(path, "w+b");
|
f = fopen(path, "w+b");
|
||||||
@@ -233,13 +251,7 @@ private:
|
|||||||
uint32_t m = h.count < cap ? h.count : cap;
|
uint32_t m = h.count < cap ? h.count : cap;
|
||||||
E* buf = m ? (E*)malloc((size_t)m * sizeof(E)) : nullptr; // PSRAM on the L2 (large block)
|
E* buf = m ? (E*)malloc((size_t)m * sizeof(E)) : nullptr; // PSRAM on the L2 (large block)
|
||||||
if (m && !buf) { fclose(f); return; }
|
if (m && !buf) { fclose(f); return; }
|
||||||
uint32_t k0 = h.count - m;
|
uint32_t got = m ? readRun(f, h, h.count - m, m, buf) : 0;
|
||||||
uint32_t got = 0;
|
|
||||||
for (uint32_t i = 0; i < m; i++) {
|
|
||||||
uint32_t slot = (h.head + k0 + i) % h.cap;
|
|
||||||
if (fseek(f, sizeof(Hdr) + (long)slot * sizeof(E), SEEK_SET) != 0 || fread(&buf[got], sizeof(E), 1, f) != 1) break;
|
|
||||||
got++;
|
|
||||||
}
|
|
||||||
fclose(f);
|
fclose(f);
|
||||||
char tmp[72];
|
char tmp[72];
|
||||||
snprintf(tmp, sizeof(tmp), "%s.tmp", path);
|
snprintf(tmp, sizeof(tmp), "%s.tmp", path);
|
||||||
@@ -323,20 +335,33 @@ private:
|
|||||||
fclose(f);
|
fclose(f);
|
||||||
}
|
}
|
||||||
std::stable_sort(picks, picks + np, [](const Pick& a, const Pick& b) { return a.ts < b.ts; });
|
std::stable_sort(picks, picks + np, [](const Pick& a, const Pick& b) { return a.ts < b.ts; });
|
||||||
int from = np > ring ? np - ring : 0;
|
int from = np > ring ? np - ring : 0, cnt = np - from;
|
||||||
for (int i = from; i < np; i++) {
|
// The chosen records read file by file (each opened once), then put
|
||||||
FILE* f = fopen(paths[picks[i].file], "rb");
|
// back in time order.
|
||||||
if (!f) continue;
|
E* recs = cnt ? (E*)malloc((size_t)cnt * sizeof(E)) : nullptr;
|
||||||
Hdr h;
|
bool* ok = cnt ? (bool*)calloc(cnt, 1) : nullptr;
|
||||||
E e;
|
int* order = cnt ? (int*)malloc((size_t)cnt * sizeof(int)) : nullptr;
|
||||||
if (readHdr<E>(f, h)) {
|
if (recs && ok && order) {
|
||||||
uint32_t first = h.next_id - h.count;
|
for (int i = 0; i < cnt; i++) order[i] = from + i;
|
||||||
uint32_t slot = (h.head + (picks[i].id - first)) % h.cap;
|
std::sort(order, order + cnt, [&](int a, int b) { return picks[a].file != picks[b].file ? picks[a].file < picks[b].file : picks[a].id < picks[b].id; });
|
||||||
if (fseek(f, sizeof(Hdr) + (long)slot * sizeof(E), SEEK_SET) == 0 && fread(&e, sizeof(E), 1, f) == 1)
|
for (int j = 0; j < cnt;) {
|
||||||
put(hist, e, slot_of[picks[i].file]);
|
int fi = picks[order[j]].file, j1 = j;
|
||||||
|
while (j1 < cnt && picks[order[j1]].file == fi) j1++;
|
||||||
|
FILE* f = fopen(paths[fi], "rb");
|
||||||
|
Hdr h;
|
||||||
|
if (f && readHdr<E>(f, h)) {
|
||||||
|
uint32_t first = h.next_id - h.count;
|
||||||
|
for (; j < j1; j++) {
|
||||||
|
int k = order[j];
|
||||||
|
ok[k - from] = picks[k].id >= first && readRun(f, h, picks[k].id - first, 1, &recs[k - from]) == 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (f) fclose(f);
|
||||||
|
j = j1;
|
||||||
}
|
}
|
||||||
fclose(f);
|
for (int i = 0; i < cnt; i++) if (ok[i]) put(hist, recs[i], slot_of[picks[from + i].file]);
|
||||||
}
|
}
|
||||||
|
free(recs); free(ok); free(order);
|
||||||
}
|
}
|
||||||
free(picks);
|
free(picks);
|
||||||
free(paths);
|
free(paths);
|
||||||
|
|||||||
@@ -139,9 +139,11 @@ static lv_obj_t* s_imgs[COLS * ROWS];
|
|||||||
static lv_obj_t* s_hint = nullptr;
|
static lv_obj_t* s_hint = nullptr;
|
||||||
static lv_obj_t* s_caption = nullptr;
|
static lv_obj_t* s_caption = nullptr;
|
||||||
static const int MAX_PTS = LiveTrackStore::CAPACITY + 2;
|
static const int MAX_PTS = LiveTrackStore::CAPACITY + 2;
|
||||||
struct Pt { int32_t lat, lon; lv_obj_t* obj; };
|
enum : uint8_t { P_ME, P_LAST, P_TARGET, P_LIVE }; // how a point's dot looks
|
||||||
|
struct Pt { int32_t lat, lon; uint8_t kind; lv_obj_t* obj; };
|
||||||
static Pt s_pts[MAX_PTS]; // [0] you, then the target, then live shares
|
static Pt s_pts[MAX_PTS]; // [0] you, then the target, then live shares
|
||||||
static int s_npts = 0;
|
static int s_npts = 0;
|
||||||
|
static bool s_fitted = false; // homeMapFit() ran for this map
|
||||||
static int s_z = mapview::DEFAULT_Z;
|
static int s_z = mapview::DEFAULT_Z;
|
||||||
static double s_cx = 0, s_cy = 0; // centre, in tiles at s_z
|
static double s_cx = 0, s_cy = 0; // centre, in tiles at s_z
|
||||||
static bool s_pending = false;
|
static bool s_pending = false;
|
||||||
@@ -150,7 +152,13 @@ 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 void onTap(lv_event_t* e) { (void)e; s_ui->openMap(true); }
|
||||||
|
|
||||||
static lv_obj_t* dot(int d, uint32_t col, uint32_t border) {
|
static lv_obj_t* dot(uint8_t kind) {
|
||||||
|
static 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
|
||||||
|
};
|
||||||
|
int d = LOOK[kind].d;
|
||||||
|
uint32_t col = LOOK[kind].col, border = LOOK[kind].border;
|
||||||
lv_obj_t* o = lv_obj_create(s_area);
|
lv_obj_t* o = lv_obj_create(s_area);
|
||||||
lv_obj_remove_style_all(o);
|
lv_obj_remove_style_all(o);
|
||||||
lv_obj_set_size(o, d, d);
|
lv_obj_set_size(o, d, d);
|
||||||
@@ -589,6 +597,7 @@ void UITask::buildHomeMap(lv_obj_t* box) {
|
|||||||
lv_image_set_pivot(s_imgs[i], 0, 0);
|
lv_image_set_pivot(s_imgs[i], 0, 0);
|
||||||
}
|
}
|
||||||
s_npts = 0;
|
s_npts = 0;
|
||||||
|
s_fitted = false;
|
||||||
s_hint = label(s_area, "", THEME_FONT_SMALL, theme::TEXT);
|
s_hint = label(s_area, "", THEME_FONT_SMALL, theme::TEXT);
|
||||||
lv_obj_set_style_bg_color(s_hint, lv_color_hex(theme::BG), 0);
|
lv_obj_set_style_bg_color(s_hint, lv_color_hex(theme::BG), 0);
|
||||||
lv_obj_set_style_bg_opa(s_hint, LV_OPA_80, 0);
|
lv_obj_set_style_bg_opa(s_hint, LV_OPA_80, 0);
|
||||||
@@ -612,42 +621,64 @@ void UITask::buildHomeMap(lv_obj_t* box) {
|
|||||||
homeMapLayout();
|
homeMapLayout();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Picks the points, the centre and the zoom that fits them.
|
// Picks the points, the centre and the zoom that fits them. False when
|
||||||
void UITask::homeMapFit() {
|
// nothing changed since the last call (no re-layout, no redraw): the dots
|
||||||
|
// stay, moved or restyled only when their points did.
|
||||||
|
bool UITask::homeMapFit() {
|
||||||
using namespace home::mini;
|
using namespace home::mini;
|
||||||
if (!s_area) return;
|
if (!s_area) return false;
|
||||||
for (int i = 0; i < s_npts; i++) if (s_pts[i].obj) lv_obj_delete(s_pts[i].obj);
|
Pt pts[MAX_PTS];
|
||||||
s_npts = 0;
|
int np = 0;
|
||||||
int32_t lat, lon;
|
int32_t lat, lon;
|
||||||
bool me = _core->course.currentLocation(lat, lon);
|
bool me = _core->course.currentLocation(lat, lon);
|
||||||
int live = 0;
|
int live = 0;
|
||||||
bool target = false;
|
bool target = false;
|
||||||
bool last = false; // no fix: the saved / app-set position, hollow
|
bool last = false; // no fix: the saved / app-set position, hollow
|
||||||
if (me) s_pts[s_npts++] = { lat, lon, dot(14, theme::ACCENT, theme::BG) };
|
if (me) pts[np++] = { lat, lon, P_ME, nullptr };
|
||||||
else if (_sensors && (_sensors->node_lat != 0 || _sensors->node_lon != 0)) {
|
else if (_sensors && (_sensors->node_lat != 0 || _sensors->node_lon != 0)) {
|
||||||
last = true;
|
last = true;
|
||||||
s_pts[s_npts++] = { (int32_t)lround(_sensors->node_lat * 1e6), (int32_t)lround(_sensors->node_lon * 1e6), dot(14, theme::BG, theme::ACCENT) };
|
pts[np++] = { (int32_t)lround(_sensors->node_lat * 1e6), (int32_t)lround(_sensors->node_lon * 1e6), P_LAST, nullptr };
|
||||||
}
|
}
|
||||||
if (_core->locator.activeTargetPos(lat, lon)) {
|
if (_core->locator.activeTargetPos(lat, lon)) {
|
||||||
target = true;
|
target = true;
|
||||||
s_pts[s_npts++] = { lat, lon, dot(12, theme::BG, theme::ACCENT) };
|
pts[np++] = { lat, lon, P_TARGET, nullptr };
|
||||||
}
|
}
|
||||||
const LiveTrackStore& lt = _core->live_share.track();
|
const LiveTrackStore& lt = _core->live_share.track();
|
||||||
uint32_t now = rtc_clock.getCurrentTime();
|
uint32_t now = rtc_clock.getCurrentTime();
|
||||||
for (int i = 0; i < LiveTrackStore::CAPACITY && s_npts < MAX_PTS; i++) {
|
for (int i = 0; i < LiveTrackStore::CAPACITY && np < MAX_PTS; i++) {
|
||||||
if (!lt.isActive(i, now)) continue;
|
if (!lt.isActive(i, now)) continue;
|
||||||
const LiveTrackStore::Entry& e = lt.slotAt(i);
|
const LiveTrackStore::Entry& e = lt.slotAt(i);
|
||||||
s_pts[s_npts++] = { e.lat_1e6, e.lon_1e6, dot(12, theme::OK, theme::BG) };
|
pts[np++] = { e.lat_1e6, e.lon_1e6, P_LIVE, nullptr };
|
||||||
live++;
|
live++;
|
||||||
}
|
}
|
||||||
|
bool same = np == s_npts;
|
||||||
|
for (int i = 0; i < np && same; i++)
|
||||||
|
same = pts[i].kind == s_pts[i].kind && pts[i].lat == s_pts[i].lat && pts[i].lon == s_pts[i].lon && s_pts[i].obj;
|
||||||
|
if (same && s_fitted) return false;
|
||||||
|
s_fitted = true;
|
||||||
|
bool made = false;
|
||||||
|
for (int i = 0; i < np; i++) { // the same dot where the kind matches
|
||||||
|
lv_obj_t* o = i < s_npts && s_pts[i].kind == pts[i].kind ? s_pts[i].obj : nullptr;
|
||||||
|
if (!o) {
|
||||||
|
if (i < s_npts && s_pts[i].obj) lv_obj_delete(s_pts[i].obj);
|
||||||
|
o = dot(pts[i].kind);
|
||||||
|
made = true;
|
||||||
|
}
|
||||||
|
pts[i].obj = o;
|
||||||
|
}
|
||||||
|
for (int i = np; i < s_npts; i++) if (s_pts[i].obj) lv_obj_delete(s_pts[i].obj);
|
||||||
|
memcpy(s_pts, pts, np * sizeof(Pt));
|
||||||
|
s_npts = np;
|
||||||
|
if (made) {
|
||||||
lv_obj_move_foreground(s_hint);
|
lv_obj_move_foreground(s_hint);
|
||||||
lv_obj_move_foreground(s_caption);
|
lv_obj_move_foreground(s_caption);
|
||||||
|
}
|
||||||
|
|
||||||
char cap[48];
|
char cap[48];
|
||||||
int o = snprintf(cap, sizeof(cap), "%s", me ? "You" : last ? "Last position" : "No position yet");
|
int o = snprintf(cap, sizeof(cap), "%s", me ? "You" : last ? "Last position" : "No position yet");
|
||||||
if (live) o += snprintf(cap + o, sizeof(cap) - o, " " LV_SYMBOL_GPS " %d sharing", live);
|
if (live) o += snprintf(cap + o, sizeof(cap) - o, " " LV_SYMBOL_GPS " %d sharing", live);
|
||||||
if (target) snprintf(cap + o, sizeof(cap) - o, " " UI_SYMBOL_FLAG " target");
|
if (target) snprintf(cap + o, sizeof(cap) - o, " " UI_SYMBOL_FLAG " target");
|
||||||
lv_label_set_text(s_caption, cap);
|
setText(s_caption, cap);
|
||||||
|
|
||||||
if (s_npts == 0) { // nothing to show: where the map was left, else the default view
|
if (s_npts == 0) { // nothing to show: where the map was left, else the default view
|
||||||
if (_map_z) {
|
if (_map_z) {
|
||||||
@@ -659,7 +690,7 @@ void UITask::homeMapFit() {
|
|||||||
s_cx = mapview::lonToTileX(mapview::DEFAULT_LON, s_z);
|
s_cx = mapview::lonToTileX(mapview::DEFAULT_LON, s_z);
|
||||||
s_cy = mapview::latToTileY(mapview::DEFAULT_LAT, s_z);
|
s_cy = mapview::latToTileY(mapview::DEFAULT_LAT, s_z);
|
||||||
}
|
}
|
||||||
return;
|
return true;
|
||||||
}
|
}
|
||||||
// Bounds in world units (tiles at z 0), then the deepest zoom they fit at.
|
// Bounds in world units (tiles at z 0), then the deepest zoom they fit at.
|
||||||
double x0 = 1e9, x1 = -1e9, y0 = 1e9, y1 = -1e9;
|
double x0 = 1e9, x1 = -1e9, y0 = 1e9, y1 = -1e9;
|
||||||
@@ -675,6 +706,7 @@ void UITask::homeMapFit() {
|
|||||||
}
|
}
|
||||||
s_cx = (x0 + x1) / 2 * (1 << s_z);
|
s_cx = (x0 + x1) / 2 * (1 << s_z);
|
||||||
s_cy = (y0 + y1) / 2 * (1 << s_z);
|
s_cy = (y0 + y1) / 2 * (1 << s_z);
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Places tiles and points; notes whether a tile still has to be decoded.
|
// Places tiles and points; notes whether a tile still has to be decoded.
|
||||||
@@ -740,8 +772,7 @@ void UITask::homeMapLoop() {
|
|||||||
if (!s_area || home::s_touching) return;
|
if (!s_area || home::s_touching) return;
|
||||||
if ((int32_t)(millis() - s_next_fit_ms) >= 0) {
|
if ((int32_t)(millis() - s_next_fit_ms) >= 0) {
|
||||||
s_next_fit_ms = millis() + 5000;
|
s_next_fit_ms = millis() + 5000;
|
||||||
homeMapFit();
|
if (homeMapFit()) homeMapLayout();
|
||||||
homeMapLayout();
|
|
||||||
}
|
}
|
||||||
if (!s_pending) return;
|
if (!s_pending) return;
|
||||||
int w = lv_obj_get_width(s_area), h = lv_obj_get_height(s_area);
|
int w = lv_obj_get_width(s_area), h = lv_obj_get_height(s_area);
|
||||||
|
|||||||
@@ -198,15 +198,22 @@ enum : uint8_t { T_CLEAR, T_WAYPOINT, T_TRAILSTART, T_LIVE };
|
|||||||
static int code(uint8_t type, int idx) { return (type << 8) | (idx & 0xFF); }
|
static int code(uint8_t type, int idx) { return (type << 8) | (idx & 0xFF); }
|
||||||
} // namespace navmap
|
} // namespace navmap
|
||||||
|
|
||||||
|
// A press only pans once it has moved DRAG_SLOP px: a tap's jitter (a pixel
|
||||||
|
// or two on the touch panel) would otherwise stop the map following you.
|
||||||
|
static const int DRAG_SLOP = 8;
|
||||||
static void onMapPress(lv_event_t* e) {
|
static void onMapPress(lv_event_t* e) {
|
||||||
|
static lv_point_t held; // movement within the slop, applied once it's a pan
|
||||||
lv_event_code_t code = lv_event_get_code(e);
|
lv_event_code_t code = lv_event_get_code(e);
|
||||||
if (code == LV_EVENT_PRESSED) { mapview::s_drag = 0; return; }
|
if (code == LV_EVENT_PRESSED) { mapview::s_drag = 0; held = { 0, 0 }; return; }
|
||||||
lv_point_t v;
|
lv_point_t v;
|
||||||
lv_indev_get_vect(lv_indev_active(), &v);
|
lv_indev_get_vect(lv_indev_active(), &v);
|
||||||
if (v.x == 0 && v.y == 0) return;
|
if (v.x == 0 && v.y == 0) return;
|
||||||
mapview::s_drag += abs(v.x) + abs(v.y);
|
mapview::s_drag += abs(v.x) + abs(v.y);
|
||||||
|
held.x += v.x; held.y += v.y;
|
||||||
|
if (mapview::s_drag <= DRAG_SLOP) return;
|
||||||
mapview::s_last_pan_ms = millis();
|
mapview::s_last_pan_ms = millis();
|
||||||
s_ui->mapPan(v.x, v.y);
|
s_ui->mapPan(held.x, held.y);
|
||||||
|
held = { 0, 0 };
|
||||||
}
|
}
|
||||||
static void onMapLongPress(lv_event_t* e) {
|
static void onMapLongPress(lv_event_t* e) {
|
||||||
if (mapview::s_drag > 8) return; // held after a pan: not a long-press on a spot
|
if (mapview::s_drag > 8) return; // held after a pan: not a long-press on a spot
|
||||||
@@ -233,6 +240,14 @@ static void onMapMarker(lv_event_t* e) {
|
|||||||
s_ui->mapOpenMarker((int)(uintptr_t)lv_event_get_user_data(e));
|
s_ui->mapOpenMarker((int)(uintptr_t)lv_event_get_user_data(e));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The centre-on-me button in the accent colour while the map follows you.
|
||||||
|
static void followShown(lv_obj_t* btn, bool on) {
|
||||||
|
if (!btn) return;
|
||||||
|
lv_obj_t* l = lv_obj_get_child(btn, 0);
|
||||||
|
lv_color_t c = lv_color_hex(on ? theme::ACCENT : theme::TEXT);
|
||||||
|
if (!lv_color_eq(lv_obj_get_style_text_color(l, LV_PART_MAIN), c)) lv_obj_set_style_text_color(l, c, 0);
|
||||||
|
}
|
||||||
|
|
||||||
static lv_obj_t* mapButton(lv_obj_t* parent, const char* text, lv_event_cb_t cb) {
|
static lv_obj_t* mapButton(lv_obj_t* parent, const char* text, lv_event_cb_t cb) {
|
||||||
lv_obj_t* b = lv_button_create(parent);
|
lv_obj_t* b = lv_button_create(parent);
|
||||||
lv_obj_set_size(b, 40, 40);
|
lv_obj_set_size(b, 40, 40);
|
||||||
@@ -351,7 +366,8 @@ void UITask::buildMap() {
|
|||||||
if (_map_nav) lv_obj_align(mapButton(body, LV_SYMBOL_BARS, onNavTools), LV_ALIGN_TOP_LEFT, 6, 98);
|
if (_map_nav) lv_obj_align(mapButton(body, LV_SYMBOL_BARS, onNavTools), LV_ALIGN_TOP_LEFT, 6, 98);
|
||||||
else lv_obj_align(mapButton(body, LV_SYMBOL_DOWNLOAD, onMapDownload), LV_ALIGN_TOP_RIGHT, -6, 98);
|
else lv_obj_align(mapButton(body, LV_SYMBOL_DOWNLOAD, onMapDownload), LV_ALIGN_TOP_RIGHT, -6, 98);
|
||||||
int bottom = _map_nav ? navmap::BAR_H : 0; // the nav bar takes the bottom edge
|
int bottom = _map_nav ? navmap::BAR_H : 0; // the nav bar takes the bottom edge
|
||||||
lv_obj_align(mapButton(body, LV_SYMBOL_GPS, onMapCenter), LV_ALIGN_BOTTOM_RIGHT, -6, -6 - bottom);
|
_map_center_btn = mapButton(body, LV_SYMBOL_GPS, onMapCenter);
|
||||||
|
lv_obj_align(_map_center_btn, LV_ALIGN_BOTTOM_RIGHT, -6, -6 - bottom);
|
||||||
_map_dl_pill = mapPill(body, ""); // tap: the download popup
|
_map_dl_pill = mapPill(body, ""); // tap: the download popup
|
||||||
lv_obj_set_style_text_color(_map_dl_pill, lv_color_hex(theme::ACCENT), 0);
|
lv_obj_set_style_text_color(_map_dl_pill, lv_color_hex(theme::ACCENT), 0);
|
||||||
lv_obj_set_style_pad_ver(_map_dl_pill, 3, 0);
|
lv_obj_set_style_pad_ver(_map_dl_pill, 3, 0);
|
||||||
@@ -485,6 +501,7 @@ void UITask::layoutMap() {
|
|||||||
} else {
|
} else {
|
||||||
lv_obj_add_flag(_map_me, LV_OBJ_FLAG_HIDDEN);
|
lv_obj_add_flag(_map_me, LV_OBJ_FLAG_HIDDEN);
|
||||||
}
|
}
|
||||||
|
followShown(_map_center_btn, _map_follow);
|
||||||
const int half = mapview::MARK_D / 2;
|
const int half = mapview::MARK_D / 2;
|
||||||
for (int k = 0; k < mapview::s_mark_count; k++) {
|
for (int k = 0; k < mapview::s_mark_count; k++) {
|
||||||
const mapview::Mark& m = mapview::s_marks[k];
|
const mapview::Mark& m = mapview::s_marks[k];
|
||||||
@@ -515,18 +532,20 @@ void UITask::layoutMap() {
|
|||||||
|
|
||||||
// One tile decode per call, nearest to the centre first.
|
// One tile decode per call, nearest to the centre first.
|
||||||
void UITask::mapLoop() {
|
void UITask::mapLoop() {
|
||||||
|
if (_map_follow && (int32_t)(millis() - _next_follow_ms) >= 0) { // following you: every fix
|
||||||
|
_next_follow_ms = millis() + 1000;
|
||||||
|
int32_t lat, lon;
|
||||||
|
if (_core->course.currentLocation(lat, lon)) {
|
||||||
|
double cx = mapview::lonToTileX(lon / 1e6, _map_z), cy = mapview::latToTileY(lat / 1e6, _map_z) + mapCenterBias();
|
||||||
|
if (cx != _map_cx || cy != _map_cy) { _map_cx = cx; _map_cy = cy; layoutMap(); }
|
||||||
|
}
|
||||||
|
}
|
||||||
if ((int32_t)(millis() - _next_map_marks_ms) >= 0) {
|
if ((int32_t)(millis() - _next_map_marks_ms) >= 0) {
|
||||||
_next_map_marks_ms = millis() + 3000;
|
_next_map_marks_ms = millis() + 3000;
|
||||||
if (_map_follow) {
|
|
||||||
int32_t lat, lon;
|
|
||||||
if (_core->course.currentLocation(lat, lon)) {
|
|
||||||
_map_cx = mapview::lonToTileX(lon / 1e6, _map_z);
|
|
||||||
_map_cy = mapview::latToTileY(lat / 1e6, _map_z) + mapCenterBias();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
rebuildMapMarkers();
|
rebuildMapMarkers();
|
||||||
layoutMap();
|
layoutMap();
|
||||||
}
|
}
|
||||||
|
if (_map_nav && navTrailSync()) layoutMap(); // a new trail point: the line keeps up with you
|
||||||
if (_map_nav) navPollAveraging();
|
if (_map_nav) navPollAveraging();
|
||||||
if (_map_nav && (int32_t)(millis() - _next_nav_bar_ms) >= 0) {
|
if (_map_nav && (int32_t)(millis() - _next_nav_bar_ms) >= 0) {
|
||||||
_next_nav_bar_ms = millis() + 1000;
|
_next_nav_bar_ms = millis() + 1000;
|
||||||
@@ -668,7 +687,7 @@ void UITask::mapZoom(int delta) {
|
|||||||
|
|
||||||
void UITask::mapCenterOnMe() {
|
void UITask::mapCenterOnMe() {
|
||||||
int32_t lat, lon;
|
int32_t lat, lon;
|
||||||
if (!ensureGps() || !_core->course.currentLocation(lat, lon)) { _map_follow = true; return; } // centres once a fix comes
|
if (!ensureGps() || !_core->course.currentLocation(lat, lon)) { _map_follow = true; followShown(_map_center_btn, true); return; } // centres once a fix comes
|
||||||
_map_follow = true;
|
_map_follow = true;
|
||||||
_map_cx = mapview::lonToTileX(lon / 1e6, _map_z);
|
_map_cx = mapview::lonToTileX(lon / 1e6, _map_z);
|
||||||
_map_cy = mapview::latToTileY(lat / 1e6, _map_z) + mapCenterBias();
|
_map_cy = mapview::latToTileY(lat / 1e6, _map_z) + mapCenterBias();
|
||||||
|
|||||||
@@ -18,19 +18,34 @@ namespace navmap {
|
|||||||
|
|
||||||
enum : uint8_t { WP_NAV, WP_RENAME, WP_SHARE, WP_DELETE };
|
enum : uint8_t { WP_NAV, WP_RENAME, WP_SHARE, WP_DELETE };
|
||||||
|
|
||||||
// Trail: normalised Web-Mercator coords (0..1) cached when the trail is
|
// Trail: normalised Web-Mercator coords (0..1) cached as points are added, so
|
||||||
// re-read, so a pan only scales and offsets them. One lv_line per recorded
|
// a redraw only scales and offsets them. Drawn by one object as a single
|
||||||
// segment (a pause starts a new one); extra segments join the last.
|
// polyline with breaks (LV_DRAW_LINE_POINT_NONE) where a pause starts a new
|
||||||
static const int TRAIL_SEGS = 32; // pauses / saves split the trail; beyond this, segments join
|
// segment and where a stretch lies outside the view: an lv_line per stretch
|
||||||
|
// would make LVGL queue one draw task per point, the queue walked to its end
|
||||||
|
// for each -- tens of thousands of points made every frame crawl.
|
||||||
|
static const int TRAIL_SEGS = 256; // pauses / saves split the trail; beyond this, segments join
|
||||||
// Stored as float offsets from the first point (s_ox/s_oy): a float holds a
|
// Stored as float offsets from the first point (s_ox/s_oy): a float holds a
|
||||||
// small offset exactly enough, but not a whole 0..1 coordinate at street zoom
|
// small offset exactly enough, but not a whole 0..1 coordinate at street zoom
|
||||||
// (2^18 * 256 px across -- a float is off by pixels there).
|
// (2^18 * 256 px across -- a float is off by pixels there).
|
||||||
static float* s_nx = nullptr; // TrailStore::CAPACITY each, in PSRAM (first trail drawn)
|
static float* s_nx = nullptr; // TrailStore::CAPACITY each, in PSRAM (first trail drawn)
|
||||||
static float* s_ny = nullptr;
|
static float* s_ny = nullptr;
|
||||||
static double s_ox = 0, s_oy = 0;
|
static double s_ox = 0, s_oy = 0;
|
||||||
|
// Bounding box of each CHUNK points (and the next chunk's first, so the line
|
||||||
|
// joining them is inside too): a chunk off the view is skipped whole.
|
||||||
|
static const int CHUNK = 64;
|
||||||
|
static const int CHUNKS = (TrailStore::CAPACITY + CHUNK - 1) / CHUNK;
|
||||||
|
static float* s_cb = nullptr; // min x, min y, max x, max y per chunk
|
||||||
|
// Screen points of the visible stretches, with breaks.
|
||||||
|
static const int PTS_MAX = TrailStore::CAPACITY + 2 * CHUNKS + TRAIL_SEGS + 4;
|
||||||
static lv_point_precise_t* s_pts = nullptr;
|
static lv_point_precise_t* s_pts = nullptr;
|
||||||
static int s_seg_first[TRAIL_SEGS], s_seg_len[TRAIL_SEGS], s_segs = 0;
|
static int s_pts_n = 0;
|
||||||
static lv_obj_t* s_trail[TRAIL_SEGS];
|
static int32_t s_pts_x0 = 0, s_pts_y0 = 0; // the trail object's screen origin s_pts was made for
|
||||||
|
static int s_seg_first[TRAIL_SEGS], s_segs = 0;
|
||||||
|
// What navTrailSync() has converted so far (s_sync_gen reset: all of it again).
|
||||||
|
static uint32_t s_sync_gen = 0xFFFFFFFF, s_sync_seq = 0, s_sync_full_at = 0;
|
||||||
|
static int s_sync_n = 0;
|
||||||
|
static lv_obj_t* s_trail = nullptr;
|
||||||
static lv_obj_t* s_target_line = nullptr;
|
static lv_obj_t* s_target_line = nullptr;
|
||||||
static lv_point_precise_t s_target_pts[2];
|
static lv_point_precise_t s_target_pts[2];
|
||||||
static lv_obj_t* s_target_ring = nullptr;
|
static lv_obj_t* s_target_ring = nullptr;
|
||||||
@@ -53,6 +68,30 @@ static double normY(int32_t lat_e6) {
|
|||||||
return (1.0 - asinh(tan(r)) / M_PI) / 2.0;
|
return (1.0 - asinh(tan(r)) / M_PI) / 2.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The trail object's draw: s_pts in one draw task (the software renderer
|
||||||
|
// walks it a segment at a time, clipped to the band being drawn).
|
||||||
|
static void onTrailDraw(lv_event_t* e) {
|
||||||
|
if (s_pts_n < 2) return;
|
||||||
|
lv_obj_t* obj = lv_event_get_current_target_obj(e);
|
||||||
|
lv_area_t a;
|
||||||
|
lv_obj_get_coords(obj, &a);
|
||||||
|
if (a.x1 != s_pts_x0 || a.y1 != s_pts_y0) { // moved since the points were made
|
||||||
|
for (int i = 0; i < s_pts_n; i++) {
|
||||||
|
if (s_pts[i].x == LV_DRAW_LINE_POINT_NONE) continue;
|
||||||
|
s_pts[i].x += a.x1 - s_pts_x0;
|
||||||
|
s_pts[i].y += a.y1 - s_pts_y0;
|
||||||
|
}
|
||||||
|
s_pts_x0 = a.x1; s_pts_y0 = a.y1;
|
||||||
|
}
|
||||||
|
lv_draw_line_dsc_t d;
|
||||||
|
lv_draw_line_dsc_init(&d);
|
||||||
|
lv_obj_init_draw_line_dsc(obj, LV_PART_MAIN, &d);
|
||||||
|
d.base.layer = lv_event_get_layer(e);
|
||||||
|
d.points = s_pts;
|
||||||
|
d.point_cnt = s_pts_n;
|
||||||
|
lv_draw_line(d.base.layer, &d);
|
||||||
|
}
|
||||||
|
|
||||||
// Waypoint labels are 11 bytes of UTF-8 (Waypoint.h): reject input past that,
|
// Waypoint labels are 11 bytes of UTF-8 (Waypoint.h): reject input past that,
|
||||||
// so a two-byte letter is never cut in half.
|
// so a two-byte letter is never cut in half.
|
||||||
static void onLabelInsert(lv_event_t* e) {
|
static void onLabelInsert(lv_event_t* e) {
|
||||||
@@ -77,16 +116,16 @@ static void onNavRenameKb(lv_event_t* e) { s_ui->navRenameDone(lv_event_get_code
|
|||||||
// ── Layers and controls (built by buildMap in nav mode) ───────────────────────
|
// ── Layers and controls (built by buildMap in nav mode) ───────────────────────
|
||||||
|
|
||||||
void UITask::buildNavLayers() {
|
void UITask::buildNavLayers() {
|
||||||
for (int i = 0; i < navmap::TRAIL_SEGS; i++) {
|
lv_obj_t* tr = lv_obj_create(_map_marks);
|
||||||
lv_obj_t* l = lv_line_create(_map_marks);
|
lv_obj_remove_style_all(tr);
|
||||||
lv_obj_set_size(l, LV_PCT(100), LV_PCT(100)); // clip to the view, whatever the points
|
lv_obj_set_size(tr, LV_PCT(100), LV_PCT(100)); // clip to the view, whatever the points
|
||||||
lv_obj_set_style_line_width(l, 3, 0);
|
lv_obj_set_style_line_width(tr, 3, 0);
|
||||||
lv_obj_set_style_line_color(l, lv_color_hex(navmap::TRAIL_COLOR), 0);
|
lv_obj_set_style_line_color(tr, lv_color_hex(navmap::TRAIL_COLOR), 0);
|
||||||
lv_obj_set_style_line_rounded(l, true, 0);
|
lv_obj_set_style_line_rounded(tr, true, 0);
|
||||||
lv_obj_remove_flag(l, LV_OBJ_FLAG_CLICKABLE);
|
lv_obj_remove_flag(tr, LV_OBJ_FLAG_CLICKABLE);
|
||||||
lv_obj_add_flag(l, LV_OBJ_FLAG_HIDDEN);
|
lv_obj_add_event_cb(tr, navmap::onTrailDraw, LV_EVENT_DRAW_MAIN, nullptr);
|
||||||
navmap::s_trail[i] = l;
|
navmap::s_trail = tr;
|
||||||
}
|
navmap::s_pts_n = 0;
|
||||||
navmap::s_target_line = lv_line_create(_map_marks);
|
navmap::s_target_line = lv_line_create(_map_marks);
|
||||||
lv_obj_set_size(navmap::s_target_line, LV_PCT(100), LV_PCT(100));
|
lv_obj_set_size(navmap::s_target_line, LV_PCT(100), LV_PCT(100));
|
||||||
lv_obj_set_style_line_width(navmap::s_target_line, 2, 0);
|
lv_obj_set_style_line_width(navmap::s_target_line, 2, 0);
|
||||||
@@ -115,6 +154,7 @@ void UITask::buildNavLayers() {
|
|||||||
lv_obj_remove_flag(navmap::s_target_dot, LV_OBJ_FLAG_CLICKABLE);
|
lv_obj_remove_flag(navmap::s_target_dot, LV_OBJ_FLAG_CLICKABLE);
|
||||||
lv_obj_center(navmap::s_target_dot);
|
lv_obj_center(navmap::s_target_dot);
|
||||||
navmap::s_segs = 0;
|
navmap::s_segs = 0;
|
||||||
|
navmap::s_sync_gen = 0xFFFFFFFF; // a new map: the whole trail converted again
|
||||||
}
|
}
|
||||||
|
|
||||||
void UITask::buildNavControls(lv_obj_t* body) {
|
void UITask::buildNavControls(lv_obj_t* body) {
|
||||||
@@ -188,56 +228,110 @@ void UITask::rebuildNavMarkers() {
|
|||||||
addMapMark(mapview::MK_LIVE, i, e.lat_1e6, e.lon_1e6, theme::OK, t);
|
addMapMark(mapview::MK_LIVE, i, e.lat_1e6, e.lon_1e6, theme::OK, t);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Trail -> normalised points in segments.
|
navTrailSync();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trail -> normalised points in segments, for layoutNav(). Only the points
|
||||||
|
// added since the last call are converted (the trail runs to tens of
|
||||||
|
// thousands of points); all of them when it was replaced (reset, load) or
|
||||||
|
// its ring, full, dropped its oldest -- at most every 10 s then.
|
||||||
|
bool UITask::navTrailSync() {
|
||||||
|
using namespace navmap;
|
||||||
TrailStore& ts = _core->trail.store();
|
TrailStore& ts = _core->trail.store();
|
||||||
navmap::s_segs = 0;
|
if (!s_pts) {
|
||||||
|
s_nx = psramBuf<float>(TrailStore::CAPACITY);
|
||||||
|
s_ny = psramBuf<float>(TrailStore::CAPACITY);
|
||||||
|
s_cb = psramBuf<float>(4 * CHUNKS);
|
||||||
|
s_pts = psramBuf<lv_point_precise_t>(PTS_MAX);
|
||||||
|
}
|
||||||
|
if (!s_nx || !s_ny || !s_cb || !s_pts) { s_segs = 0; s_sync_n = 0; return false; }
|
||||||
int n = ts.count();
|
int n = ts.count();
|
||||||
if (!navmap::s_pts) {
|
if (ts.gen() == s_sync_gen && ts.seq() == s_sync_seq && n == s_sync_n) return false;
|
||||||
navmap::s_nx = psramBuf<float>(TrailStore::CAPACITY);
|
bool all = ts.gen() != s_sync_gen || n < s_sync_n || n - s_sync_n != (int)(ts.seq() - s_sync_seq);
|
||||||
navmap::s_ny = psramBuf<float>(TrailStore::CAPACITY);
|
if (all && ts.gen() == s_sync_gen && n == TrailStore::CAPACITY && millis() - s_sync_full_at < 10000) return false;
|
||||||
navmap::s_pts = psramBuf<lv_point_precise_t>(TrailStore::CAPACITY);
|
int from = all ? 0 : s_sync_n;
|
||||||
}
|
if (all) { s_segs = 0; s_sync_full_at = millis(); }
|
||||||
if (!navmap::s_nx || !navmap::s_ny || !navmap::s_pts) n = 0;
|
for (int i = from; i < n; i++) {
|
||||||
for (int i = 0; i < n; i++) {
|
|
||||||
const TrailPoint& p = ts.at(i);
|
const TrailPoint& p = ts.at(i);
|
||||||
if (i == 0) { navmap::s_ox = navmap::normX(p.lon_1e6); navmap::s_oy = navmap::normY(p.lat_1e6); }
|
if (i == 0) { s_ox = normX(p.lon_1e6); s_oy = normY(p.lat_1e6); }
|
||||||
navmap::s_nx[i] = (float)(navmap::normX(p.lon_1e6) - navmap::s_ox);
|
float x = s_nx[i] = (float)(normX(p.lon_1e6) - s_ox);
|
||||||
navmap::s_ny[i] = (float)(navmap::normY(p.lat_1e6) - navmap::s_oy);
|
float y = s_ny[i] = (float)(normY(p.lat_1e6) - s_oy);
|
||||||
bool new_seg = i == 0 || ((p.flags & TRAIL_FLAG_SEG_START) && navmap::s_segs < navmap::TRAIL_SEGS);
|
float* b = &s_cb[4 * (i / CHUNK)];
|
||||||
if (new_seg) {
|
if (i % CHUNK == 0) { b[0] = b[2] = x; b[1] = b[3] = y; }
|
||||||
navmap::s_seg_first[navmap::s_segs] = i;
|
for (int k = i % CHUNK == 0 && i > 0 ? 2 : 1; k > 0; k--, b -= 4) { // this chunk; a first point also closes the last
|
||||||
navmap::s_seg_len[navmap::s_segs] = 0;
|
if (x < b[0]) b[0] = x;
|
||||||
navmap::s_segs++;
|
if (y < b[1]) b[1] = y;
|
||||||
|
if (x > b[2]) b[2] = x;
|
||||||
|
if (y > b[3]) b[3] = y;
|
||||||
}
|
}
|
||||||
navmap::s_seg_len[navmap::s_segs - 1]++;
|
if (i == 0 || ((p.flags & TRAIL_FLAG_SEG_START) && s_segs < TRAIL_SEGS)) s_seg_first[s_segs++] = i;
|
||||||
}
|
}
|
||||||
|
s_sync_gen = ts.gen();
|
||||||
|
s_sync_seq = ts.seq();
|
||||||
|
s_sync_n = n;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The trail's screen points for layoutNav(): only the chunks whose box meets
|
||||||
|
// the view, and of those only points 2 px or more from the last kept one
|
||||||
|
// (zoomed out, thousands of points shrink to the few hundred that show). A
|
||||||
|
// point dropped that way still ends its stretch, so the line reaches it.
|
||||||
|
static void layoutNavTrail(double scale) {
|
||||||
|
using namespace navmap;
|
||||||
|
s_pts_n = 0;
|
||||||
|
if (!s_trail) return;
|
||||||
|
lv_obj_invalidate(s_trail);
|
||||||
|
int n = s_sync_n;
|
||||||
|
if (!s_pts || n < 2) return;
|
||||||
|
lv_area_t a;
|
||||||
|
lv_obj_get_coords(s_trail, &a);
|
||||||
|
s_pts_x0 = a.x1; s_pts_y0 = a.y1;
|
||||||
|
const double M = 8; // past the edge: the line's width, rounded caps
|
||||||
|
float fs = (float)scale;
|
||||||
|
float ox = (float)(s_ox * scale - mapview::s_left) + a.x1;
|
||||||
|
float oy = (float)(s_oy * scale - mapview::s_top) + a.y1;
|
||||||
|
float vx0 = (float)((mapview::s_left - M) / scale - s_ox), vx1 = (float)((mapview::s_left + lv_area_get_width(&a) + M) / scale - s_ox);
|
||||||
|
float vy0 = (float)((mapview::s_top - M) / scale - s_oy), vy1 = (float)((mapview::s_top + lv_area_get_height(&a) + M) / scale - s_oy);
|
||||||
|
|
||||||
|
bool open = false, held = false; // a stretch is being drawn; a thinned-out point is owed at its end
|
||||||
|
lv_point_precise_t hold = {0, 0}, last = {0, 0};
|
||||||
|
auto push = [&](lv_value_precise_t x, lv_value_precise_t y) { s_pts[s_pts_n].x = x; s_pts[s_pts_n].y = y; s_pts_n++; };
|
||||||
|
auto endRun = [&]() {
|
||||||
|
if (held) push(hold.x, hold.y);
|
||||||
|
if (open) push(LV_DRAW_LINE_POINT_NONE, LV_DRAW_LINE_POINT_NONE);
|
||||||
|
open = held = false;
|
||||||
|
};
|
||||||
|
auto emit = [&](int i) {
|
||||||
|
lv_value_precise_t x = (lv_value_precise_t)lroundf(s_nx[i] * fs + ox);
|
||||||
|
lv_value_precise_t y = (lv_value_precise_t)lroundf(s_ny[i] * fs + oy);
|
||||||
|
if (open && fabsf((float)(x - last.x)) < 2 && fabsf((float)(y - last.y)) < 2) { hold.x = x; hold.y = y; held = true; return; }
|
||||||
|
push(x, y);
|
||||||
|
last.x = x; last.y = y;
|
||||||
|
open = true; held = false;
|
||||||
|
};
|
||||||
|
int seg = 1; // s_seg_first[0] is the first point
|
||||||
|
for (int i0 = 0; i0 < n; i0 += CHUNK) {
|
||||||
|
int i1 = i0 + CHUNK < n ? i0 + CHUNK : n;
|
||||||
|
const float* b = &s_cb[4 * (i0 / CHUNK)];
|
||||||
|
bool starts = seg < s_segs && s_seg_first[seg] == i0;
|
||||||
|
if (b[2] < vx0 || b[0] > vx1 || b[3] < vy0 || b[1] > vy1) { // off the view
|
||||||
|
if (open && !starts) emit(i0); // the last chunk's line to this one's first point
|
||||||
|
endRun();
|
||||||
|
while (seg < s_segs && s_seg_first[seg] < i1) seg++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (int i = i0; i < i1; i++) {
|
||||||
|
if (seg < s_segs && s_seg_first[seg] == i) { endRun(); seg++; }
|
||||||
|
emit(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
endRun();
|
||||||
}
|
}
|
||||||
|
|
||||||
void UITask::layoutNav() {
|
void UITask::layoutNav() {
|
||||||
if (!navmap::s_target_line) return;
|
if (!navmap::s_target_line) return;
|
||||||
double scale = (double)(1 << _map_z) * mapview::TILE_PX;
|
double scale = (double)(1 << _map_z) * mapview::TILE_PX;
|
||||||
for (int s = 0; s < navmap::TRAIL_SEGS; s++) {
|
layoutNavTrail(scale);
|
||||||
lv_obj_t* l = navmap::s_trail[s];
|
|
||||||
if (s >= navmap::s_segs || navmap::s_seg_len[s] < 2) { lv_obj_add_flag(l, LV_OBJ_FLAG_HIDDEN); continue; }
|
|
||||||
// Points closer than 2 px to the last kept one are dropped (the last
|
|
||||||
// point always stays): zoomed out, thousands of points shrink to the few
|
|
||||||
// hundred that show, so a long trail doesn't slow every map redraw.
|
|
||||||
int f = navmap::s_seg_first[s], len = navmap::s_seg_len[s], k = 0;
|
|
||||||
for (int i = f; i < f + len; i++) {
|
|
||||||
lv_value_precise_t x = (lv_value_precise_t)lround((navmap::s_ox + navmap::s_nx[i]) * scale - mapview::s_left);
|
|
||||||
lv_value_precise_t y = (lv_value_precise_t)lround((navmap::s_oy + navmap::s_ny[i]) * scale - mapview::s_top);
|
|
||||||
if (k > 0 && i < f + len - 1) {
|
|
||||||
lv_point_precise_t& last = navmap::s_pts[f + k - 1];
|
|
||||||
if (fabsf((float)(x - last.x)) < 2 && fabsf((float)(y - last.y)) < 2) continue;
|
|
||||||
}
|
|
||||||
navmap::s_pts[f + k].x = x;
|
|
||||||
navmap::s_pts[f + k].y = y;
|
|
||||||
k++;
|
|
||||||
}
|
|
||||||
if (k < 2) { lv_obj_add_flag(l, LV_OBJ_FLAG_HIDDEN); continue; }
|
|
||||||
lv_line_set_points(l, &navmap::s_pts[f], k);
|
|
||||||
lv_obj_remove_flag(l, LV_OBJ_FLAG_HIDDEN);
|
|
||||||
}
|
|
||||||
|
|
||||||
int32_t tlat, tlon, mlat, mlon;
|
int32_t tlat, tlon, mlat, mlon;
|
||||||
char name[8];
|
char name[8];
|
||||||
@@ -819,6 +913,7 @@ static void scanTrails() {
|
|||||||
while (struct dirent* de = readdir(d)) {
|
while (struct dirent* de = readdir(d)) {
|
||||||
size_t l = strlen(de->d_name);
|
size_t l = strlen(de->d_name);
|
||||||
if (l < 5 || l >= sizeof(s_st_names[0]) || strcmp(de->d_name + l - 4, ".trl") != 0) continue;
|
if (l < 5 || l >= sizeof(s_st_names[0]) || strcmp(de->d_name + l - 4, ".trl") != 0) continue;
|
||||||
|
if (de->d_name[0] == '.') continue; // the live trail's own copy (.live.trl)
|
||||||
if (s_st_n < ST_MAX) snprintf(s_st_names[s_st_n++], sizeof(s_st_names[0]), "%s", de->d_name);
|
if (s_st_n < ST_MAX) snprintf(s_st_names[s_st_n++], sizeof(s_st_names[0]), "%s", de->d_name);
|
||||||
}
|
}
|
||||||
closedir(d);
|
closedir(d);
|
||||||
@@ -1015,6 +1110,75 @@ bool UITask::saveTrailToCard(char* name_out, size_t n) {
|
|||||||
return ok;
|
return ok;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── The live trail's copy on the card ──
|
||||||
|
// While there's a trail, /sdcard/trails/.live.trl holds a copy of it: the new
|
||||||
|
// points appended once a minute and at power-off, so a flat battery, a crash
|
||||||
|
// or a reboot doesn't lose the route. At boot the copy comes back, recording
|
||||||
|
// again if it was. A saved trail's format, the header's reserved byte (5)
|
||||||
|
// saying "recording"; count and time (6, 8) are patched in place. A full
|
||||||
|
// ring (the oldest points dropping off) is rewritten, at most every 10 min.
|
||||||
|
namespace navmap {
|
||||||
|
static const char* const LIVE_TRAIL = "/sdcard/trails/.live.trl";
|
||||||
|
static const long LIVE_HDR = 12; // magic, version, reserved, count (uint16), time (uint32)
|
||||||
|
static uint32_t s_j_gen = 0xFFFFFFFF, s_j_seq = 0, s_j_next = 0, s_j_full_at = 0;
|
||||||
|
static int s_j_n = 0;
|
||||||
|
static uint8_t s_j_rec = 0; // the "recording" byte last written
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UITask::trailJournalTick(bool now) {
|
||||||
|
using namespace navmap;
|
||||||
|
if (!now && (int32_t)(millis() - s_j_next) < 0) return true;
|
||||||
|
s_j_next = millis() + 60000;
|
||||||
|
TrailStore& ts = _core->trail.store();
|
||||||
|
int n = ts.count();
|
||||||
|
uint8_t rec = ts.isActive() ? 1 : 0;
|
||||||
|
bool same = ts.gen() == s_j_gen && ts.seq() == s_j_seq && n == s_j_n;
|
||||||
|
if (same && (n == 0 || (!rec && s_j_rec == rec))) return true; // nothing to write (and no card mount tried)
|
||||||
|
if (!lvport::mountStorage()) return false;
|
||||||
|
if (n == 0) { // reset: no copy
|
||||||
|
if (s_j_n) remove(LIVE_TRAIL); // only a copy this run wrote (or restored)
|
||||||
|
s_j_gen = ts.gen(); s_j_seq = ts.seq(); s_j_n = 0;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
bool all = ts.gen() != s_j_gen || n < s_j_n || n - s_j_n != (int)(ts.seq() - s_j_seq);
|
||||||
|
if (all && ts.gen() == s_j_gen && n == TrailStore::CAPACITY && !now && millis() - s_j_full_at < 600000) return true;
|
||||||
|
FILE* f = all ? nullptr : fopen(LIVE_TRAIL, "r+b");
|
||||||
|
bool ok;
|
||||||
|
if (!f) { // the whole trail
|
||||||
|
mapview::makeParents(LIVE_TRAIL);
|
||||||
|
f = fopen(LIVE_TRAIL, "wb");
|
||||||
|
if (!f) return false;
|
||||||
|
navmap::FileRW io{ f };
|
||||||
|
ok = ts.writeTo(io);
|
||||||
|
s_j_full_at = millis();
|
||||||
|
} else { // just what's new
|
||||||
|
ok = fseek(f, 0, SEEK_END) == 0;
|
||||||
|
for (int i = s_j_n; ok && i < n; i++) ok = fwrite(&ts.at(i), sizeof(TrailPoint), 1, f) == 1;
|
||||||
|
}
|
||||||
|
uint16_t cnt = (uint16_t)n;
|
||||||
|
uint32_t accum = ts.currentAccumulatedMs();
|
||||||
|
ok = ok && fseek(f, 5, SEEK_SET) == 0 && fwrite(&rec, 1, 1, f) == 1 && fwrite(&cnt, 2, 1, f) == 1 && fwrite(&accum, 4, 1, f) == 1;
|
||||||
|
ok = (fclose(f) == 0) && ok;
|
||||||
|
if (ok) { s_j_gen = ts.gen(); s_j_seq = ts.seq(); s_j_n = n; s_j_rec = rec; }
|
||||||
|
else s_j_gen = 0xFFFFFFFF; // next time the whole trail again
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
void UITask::trailJournalRestore() {
|
||||||
|
using namespace navmap;
|
||||||
|
TrailStore& ts = _core->trail.store();
|
||||||
|
if (!ts.empty() || !lvport::mountStorage()) return;
|
||||||
|
FILE* f = fopen(LIVE_TRAIL, "rb");
|
||||||
|
if (!f) return;
|
||||||
|
uint8_t rec = 0;
|
||||||
|
navmap::FileRW io{ f };
|
||||||
|
bool ok = ts.readFrom(io) && fseek(f, 5, SEEK_SET) == 0 && fread(&rec, 1, 1, f) == 1;
|
||||||
|
fclose(f);
|
||||||
|
if (!ok || ts.empty()) return;
|
||||||
|
if (rec & 1) ts.setActive(true); // it was recording: carries on (a gap for the time off)
|
||||||
|
s_j_gen = ts.gen(); s_j_seq = ts.seq(); s_j_n = ts.count(); s_j_rec = rec & 1;
|
||||||
|
}
|
||||||
|
|
||||||
// Map tools > Load, with a card: the saved trails, newest first, and the
|
// Map tools > Load, with a card: the saved trails, newest first, and the
|
||||||
// device's own slot (manual saves without a card, the low-battery auto-save).
|
// device's own slot (manual saves without a card, the low-battery auto-save).
|
||||||
void UITask::savedTrailsPopup() {
|
void UITask::savedTrailsPopup() {
|
||||||
|
|||||||
@@ -893,6 +893,7 @@ void UITask::begin(DisplayDriver* display_drv, SensorManager* sensors, NodePrefs
|
|||||||
s_archive.restore(_core->history);
|
s_archive.restore(_core->history);
|
||||||
_core->history.setArchive(&s_archive);
|
_core->history.setArchive(&s_archive);
|
||||||
}
|
}
|
||||||
|
trailJournalRestore(); // a trail cut off by a restart or a flat battery
|
||||||
_tap_wake = lvport::loadTapWake();
|
_tap_wake = lvport::loadTapWake();
|
||||||
msgtext::seedQuick(node_prefs); // "OK" in quick message 1 on first boot
|
msgtext::seedQuick(node_prefs); // "OK" in quick message 1 on first boot
|
||||||
_nearby = new NearbyModel();
|
_nearby = new NearbyModel();
|
||||||
@@ -1080,6 +1081,7 @@ void UITask::loop() {
|
|||||||
checkLowBattery();
|
checkLowBattery();
|
||||||
mapDownloadTick(); // a map download keeps running on any screen, and asleep
|
mapDownloadTick(); // a map download keeps running on any screen, and asleep
|
||||||
otaTick(); // so does a firmware update
|
otaTick(); // so does a firmware update
|
||||||
|
trailJournalTick(); // the live trail's copy on the card, once a minute
|
||||||
if ((int32_t)(millis() - _next_trackback_ms) >= 0) { // walking the trail back, on any screen
|
if ((int32_t)(millis() - _next_trackback_ms) >= 0) { // walking the trail back, on any screen
|
||||||
_next_trackback_ms = millis() + 1000;
|
_next_trackback_ms = millis() + 1000;
|
||||||
navPollTrackBack();
|
navPollTrackBack();
|
||||||
@@ -1152,7 +1154,7 @@ void UITask::shutdown(bool restart) {
|
|||||||
the_mesh.savePrefs();
|
the_mesh.savePrefs();
|
||||||
the_mesh.saveRTCTime();
|
the_mesh.saveRTCTime();
|
||||||
the_mesh.flushDirtyContacts();
|
the_mesh.flushDirtyContacts();
|
||||||
_core->trail.onShutdown();
|
if (!trailJournalTick(true)) _core->trail.onShutdown(); // no card: the internal slot, if enabled
|
||||||
#ifdef PIN_BUZZER
|
#ifdef PIN_BUZZER
|
||||||
_buzzer.shutdown(); // the goodbye sound, unless muted
|
_buzzer.shutdown(); // the goodbye sound, unless muted
|
||||||
#ifndef SIM_PLATFORM // the sim's single thread would freeze the page
|
#ifndef SIM_PLATFORM // the sim's single thread would freeze the page
|
||||||
@@ -1667,16 +1669,19 @@ static uint32_t s_wifi_test_ms = 0; // Settings > WiFi checking a just-saved n
|
|||||||
|
|
||||||
namespace home { static void leave(); } // HomeScreen.h
|
namespace home { static void leave(); } // HomeScreen.h
|
||||||
|
|
||||||
|
static int s_th_rows_n = 0; // conversation bubbles built (refreshThread)
|
||||||
|
|
||||||
lv_obj_t* UITask::newScreen(const char* title, bool with_back) {
|
lv_obj_t* UITask::newScreen(const char* title, bool with_back) {
|
||||||
_home_clock = _home_date = _home_unread = nullptr;
|
_home_clock = _home_date = _home_unread = nullptr;
|
||||||
_thread_list = _compose_ta = _keyboard = nullptr;
|
_thread_list = _compose_ta = _keyboard = nullptr;
|
||||||
|
s_th_rows_n = 0; // the bubbles went with the screen
|
||||||
if (_shown_screen == SCR_SETTINGS && _body) s_settings_y = lv_obj_get_scroll_y(_body);
|
if (_shown_screen == SCR_SETTINGS && _body) s_settings_y = lv_obj_get_scroll_y(_body);
|
||||||
pickerClose();
|
pickerClose();
|
||||||
_header = _body = nullptr;
|
_header = _body = nullptr;
|
||||||
_nearby_list = _nearby_status = _nearby_sort_lbl = _nearby_chips = nullptr;
|
_nearby_list = _nearby_status = _nearby_sort_lbl = _nearby_chips = nullptr;
|
||||||
_node_info = _node_ping = _node_delete_lbl = nullptr;
|
_node_info = _node_ping = _node_delete_lbl = nullptr;
|
||||||
_scan_overlay = _scan_list = _scan_status = nullptr; // the popup went with the old screen
|
_scan_overlay = _scan_list = _scan_status = nullptr; // the popup went with the old screen
|
||||||
_map_area = _map_marks = _map_me = _map_zoom_lbl = _map_hint = _map_dl_pill = nullptr;
|
_map_area = _map_marks = _map_me = _map_zoom_lbl = _map_hint = _map_dl_pill = _map_center_btn = nullptr;
|
||||||
_dl_overlay = _dl_info = _dl_zoom_lbl = _dl_start_lbl = _dl_job_row = _dl_job_lbl = nullptr;
|
_dl_overlay = _dl_info = _dl_zoom_lbl = _dl_start_lbl = _dl_job_row = _dl_job_lbl = nullptr;
|
||||||
_nav_bar = _nav_title = _nav_info = _nav_clear = nullptr;
|
_nav_bar = _nav_title = _nav_info = _nav_clear = nullptr;
|
||||||
_nav_overlay = _nav_ta = _nav_kb = _nav_del_lbl = _nav_rec = _nav_avg_pill = nullptr;
|
_nav_overlay = _nav_ta = _nav_kb = _nav_del_lbl = _nav_rec = _nav_avg_pill = nullptr;
|
||||||
@@ -2343,8 +2348,12 @@ void UITask::refreshNearbyList() {
|
|||||||
if (sig == _nearby_sig) return;
|
if (sig == _nearby_sig) return;
|
||||||
_nearby_sig = sig;
|
_nearby_sig = sig;
|
||||||
|
|
||||||
|
// The same number of nodes (the usual case: ages, distances, signal
|
||||||
|
// change): the rows rewritten in place, not built again.
|
||||||
|
bool in_place = n > 0 && (int)lv_obj_get_child_count(_nearby_list) == n
|
||||||
|
&& lv_obj_check_type(lv_obj_get_child(_nearby_list, 0), &lv_button_class);
|
||||||
int32_t scroll = lv_obj_get_scroll_y(_nearby_list);
|
int32_t scroll = lv_obj_get_scroll_y(_nearby_list);
|
||||||
lv_obj_clean(_nearby_list);
|
if (!in_place) lv_obj_clean(_nearby_list);
|
||||||
uint32_t now = rtc_clock.getCurrentTime();
|
uint32_t now = rtc_clock.getCurrentTime();
|
||||||
bool imperial = _prefs && _prefs->units_imperial;
|
bool imperial = _prefs && _prefs->units_imperial;
|
||||||
for (int i = 0; i < n; i++) {
|
for (int i = 0; i < n; i++) {
|
||||||
@@ -2359,13 +2368,33 @@ void UITask::refreshNearbyList() {
|
|||||||
const char* kind = e.contact_idx >= 0 ? NearbyModel::typeName(e.type) : "not a contact";
|
const char* kind = e.contact_idx >= 0 ? NearbyModel::typeName(e.type) : "not a contact";
|
||||||
snprintf(sub, sizeof(sub), "%s%s%s%s", kind, e.is_live ? " - live" : "",
|
snprintf(sub, sizeof(sub), "%s%s%s%s", kind, e.is_live ? " - live" : "",
|
||||||
(e.dist_km >= 0.0f && age[0]) ? " - " : "", (e.dist_km >= 0.0f && age[0]) ? age : "");
|
(e.dist_km >= 0.0f && age[0]) ? " - " : "", (e.dist_km >= 0.0f && age[0]) ? age : "");
|
||||||
|
uint32_t right_col = e.is_live ? theme::OK : theme::TEXT_MUTED;
|
||||||
|
if (in_place) {
|
||||||
|
lv_obj_t* row = lv_obj_get_child(_nearby_list, i);
|
||||||
|
lv_obj_t* t = rowTitle(row);
|
||||||
|
setText(t, title);
|
||||||
|
lv_color_t tc = lv_color_hex(e.fav ? theme::ACCENT : theme::TEXT);
|
||||||
|
if (!lv_color_eq(lv_obj_get_style_text_color(t, LV_PART_MAIN), tc)) lv_obj_set_style_text_color(t, tc, 0);
|
||||||
|
setText(rowSub(row), sub);
|
||||||
|
lv_obj_t* r = lv_obj_get_child(row, 2);
|
||||||
|
if (!r && right[0]) {
|
||||||
|
r = label(row, right, THEME_FONT_SMALL, right_col);
|
||||||
|
lv_obj_align(r, LV_ALIGN_RIGHT_MID, -theme::PAD, 0);
|
||||||
|
} else if (r) {
|
||||||
|
setText(r, right);
|
||||||
|
lv_color_t rc = lv_color_hex(right_col);
|
||||||
|
if (!lv_color_eq(lv_obj_get_style_text_color(r, LV_PART_MAIN), rc)) lv_obj_set_style_text_color(r, rc, 0);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
lv_obj_t* row = listRow(_nearby_list, title, sub, onNearbyRow, (void*)(uintptr_t)i);
|
lv_obj_t* row = listRow(_nearby_list, title, sub, onNearbyRow, (void*)(uintptr_t)i);
|
||||||
if (e.fav) lv_obj_set_style_text_color(rowTitle(row), lv_color_hex(theme::ACCENT), 0);
|
if (e.fav) lv_obj_set_style_text_color(rowTitle(row), lv_color_hex(theme::ACCENT), 0);
|
||||||
if (right[0]) {
|
if (right[0]) {
|
||||||
lv_obj_t* r = label(row, right, THEME_FONT_SMALL, e.is_live ? theme::OK : theme::TEXT_MUTED);
|
lv_obj_t* r = label(row, right, THEME_FONT_SMALL, right_col);
|
||||||
lv_obj_align(r, LV_ALIGN_RIGHT_MID, -theme::PAD, 0);
|
lv_obj_align(r, LV_ALIGN_RIGHT_MID, -theme::PAD, 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (in_place) return;
|
||||||
if (n == 0) {
|
if (n == 0) {
|
||||||
lv_obj_t* l = label(_nearby_list, "Nobody here yet. Tap Scan to look around.",
|
lv_obj_t* l = label(_nearby_list, "Nobody here yet. Tap Scan to look around.",
|
||||||
THEME_FONT_BODY, theme::TEXT_MUTED);
|
THEME_FONT_BODY, theme::TEXT_MUTED);
|
||||||
@@ -2672,6 +2701,7 @@ void UITask::buildThread() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_thread_list = scrollList(body);
|
_thread_list = scrollList(body);
|
||||||
|
s_th_rows_n = 0;
|
||||||
lv_obj_set_style_pad_all(_thread_list, theme::PAD, 0);
|
lv_obj_set_style_pad_all(_thread_list, theme::PAD, 0);
|
||||||
|
|
||||||
_compose_ta = nullptr;
|
_compose_ta = nullptr;
|
||||||
@@ -2750,25 +2780,33 @@ static ChHistEntry* s_th_ch = psramBuf<ChHistEntry>(THREAD_MAX_SHOWN);
|
|||||||
static DmHistEntry* s_th_dm = psramBuf<DmHistEntry>(THREAD_MAX_SHOWN);
|
static DmHistEntry* s_th_dm = psramBuf<DmHistEntry>(THREAD_MAX_SHOWN);
|
||||||
|
|
||||||
// What a held bubble is about: its entry (index into s_th_ch / s_th_dm),
|
// What a held bubble is about: its entry (index into s_th_ch / s_th_dm),
|
||||||
// sender, position slot.
|
// sender, position slot. One per shown message, at its index.
|
||||||
struct MsgMeta { int pos; bool channel; bool own; int loc; char from[32]; };
|
struct MsgMeta { int pos; bool channel; bool own; int loc; char from[32]; };
|
||||||
static MsgMeta* s_msg_meta = psramBuf<MsgMeta>(THREAD_MAX_SHOWN);
|
static MsgMeta* s_msg_meta = psramBuf<MsgMeta>(THREAD_MAX_SHOWN);
|
||||||
static int s_msg_meta_n = 0;
|
static int s_msg_meta_n = 0;
|
||||||
|
|
||||||
static int noteMsgMeta(int pos, bool channel, bool own, int loc, const char* from) {
|
// The bubbles built, one per shown message: which message (key), its
|
||||||
if (s_msg_meta_n >= THREAD_MAX_SHOWN) return -1;
|
// delivery state, its row and age label. A refresh that only adds messages
|
||||||
MsgMeta& m = s_msg_meta[s_msg_meta_n];
|
// or changes a delivery mark touches just those bubbles -- rebuilding all 50
|
||||||
m.pos = pos; m.channel = channel; m.own = own; m.loc = loc;
|
// on every repeater echo made the conversation stutter.
|
||||||
snprintf(m.from, sizeof(m.from), "%s", from ? from : "");
|
struct ThreadRow { uint32_t key, state; lv_obj_t* row; lv_obj_t* age; };
|
||||||
return s_msg_meta_n++;
|
static ThreadRow* s_th_rows = psramBuf<ThreadRow>(THREAD_MAX_SHOWN);
|
||||||
|
static lv_obj_t* s_older_lbl = nullptr; // "Older messages (n)" on the page's top
|
||||||
|
static bool s_th_rows_newest = false; // built for the newest page (the one patched)
|
||||||
|
|
||||||
|
// The shown message a bubble (or a button in it) belongs to, or -1.
|
||||||
|
static int threadRowOf(lv_obj_t* o) {
|
||||||
|
for (; o; o = lv_obj_get_parent(o))
|
||||||
|
for (int r = 0; r < s_th_rows_n; r++) if (s_th_rows[r].row == o) return r;
|
||||||
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
static void onMsgLoc(lv_event_t* e) {
|
static void onMsgLoc(lv_event_t* e) {
|
||||||
uintptr_t v = (uintptr_t)lv_event_get_user_data(e);
|
int r = threadRowOf((lv_obj_t*)lv_event_get_current_target(e));
|
||||||
s_ui->messageLocationAction((int)(v >> 1), (v & 1) != 0);
|
if (r >= 0) s_ui->messageLocationAction(s_msg_meta[r].loc, (uintptr_t)lv_event_get_user_data(e) != 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void msgLocButton(lv_obj_t* parent, const char* text, int idx, bool save, bool accent) {
|
static void msgLocButton(lv_obj_t* parent, const char* text, bool save, bool accent) {
|
||||||
lv_obj_t* b = lv_button_create(parent);
|
lv_obj_t* b = lv_button_create(parent);
|
||||||
lv_obj_set_size(b, LV_SIZE_CONTENT, 28);
|
lv_obj_set_size(b, LV_SIZE_CONTENT, 28);
|
||||||
lv_obj_set_style_pad_hor(b, 10, 0);
|
lv_obj_set_style_pad_hor(b, 10, 0);
|
||||||
@@ -2776,7 +2814,7 @@ static void msgLocButton(lv_obj_t* parent, const char* text, int idx, bool save,
|
|||||||
lv_obj_set_style_radius(b, LV_RADIUS_CIRCLE, 0);
|
lv_obj_set_style_radius(b, LV_RADIUS_CIRCLE, 0);
|
||||||
lv_obj_set_style_shadow_width(b, 0, 0);
|
lv_obj_set_style_shadow_width(b, 0, 0);
|
||||||
lv_obj_set_style_bg_color(b, lv_color_hex(accent ? theme::ACCENT : theme::SURFACE_2), 0);
|
lv_obj_set_style_bg_color(b, lv_color_hex(accent ? theme::ACCENT : theme::SURFACE_2), 0);
|
||||||
lv_obj_add_event_cb(b, onMsgLoc, LV_EVENT_CLICKED, (void*)(uintptr_t)((idx << 1) | (save ? 1 : 0)));
|
lv_obj_add_event_cb(b, onMsgLoc, LV_EVENT_CLICKED, (void*)(uintptr_t)(save ? 1 : 0));
|
||||||
lv_obj_center(label(b, text, THEME_FONT_SMALL, accent ? theme::BG : theme::TEXT));
|
lv_obj_center(label(b, text, THEME_FONT_SMALL, accent ? theme::BG : theme::TEXT));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2834,10 +2872,11 @@ static lv_obj_t* msgText(lv_obj_t* parent, const char* text, bool own, int max_w
|
|||||||
}
|
}
|
||||||
|
|
||||||
// One message bubble. Own messages right-aligned in amber, others left.
|
// One message bubble. Own messages right-aligned in amber, others left.
|
||||||
// loc_idx >= 0: the text carries a position (s_msg_locs[loc_idx]).
|
// loc: the text carries a position (Go / Save buttons); hold: a long press
|
||||||
static void bubble(lv_obj_t* list, const char* from, const char* text, bool own,
|
// opens the message menu. Returns its row; *age_out = the age label.
|
||||||
uint32_t ts, const char* status, uint32_t status_col, int loc_idx = -1, int meta_idx = -1,
|
static lv_obj_t* bubble(lv_obj_t* list, const char* from, const char* text, bool own,
|
||||||
int relays = 0) {
|
uint32_t ts, const char* status, uint32_t status_col, bool loc, bool hold,
|
||||||
|
int relays, lv_obj_t** age_out) {
|
||||||
// Full-width row that pushes the bubble to its side.
|
// Full-width row that pushes the bubble to its side.
|
||||||
lv_obj_t* row = lv_obj_create(list);
|
lv_obj_t* row = lv_obj_create(list);
|
||||||
styleSurface(row, theme::BG);
|
styleSurface(row, theme::BG);
|
||||||
@@ -2851,8 +2890,8 @@ static void bubble(lv_obj_t* list, const char* from, const char* text, bool own,
|
|||||||
|
|
||||||
lv_obj_t* b = lv_obj_create(row);
|
lv_obj_t* b = lv_obj_create(row);
|
||||||
lv_obj_remove_flag(b, LV_OBJ_FLAG_SCROLLABLE);
|
lv_obj_remove_flag(b, LV_OBJ_FLAG_SCROLLABLE);
|
||||||
if (meta_idx >= 0) { // hold: reply / path / target (ConversationScreen.h)
|
if (hold) { // hold: reply / path / target (ConversationScreen.h)
|
||||||
lv_obj_add_event_cb(b, onMsgHold, LV_EVENT_LONG_PRESSED, (void*)(uintptr_t)meta_idx);
|
lv_obj_add_event_cb(b, onMsgHold, LV_EVENT_LONG_PRESSED, nullptr);
|
||||||
lv_obj_set_style_bg_color(b, lv_color_hex(theme::SURFACE_2), LV_STATE_PRESSED);
|
lv_obj_set_style_bg_color(b, lv_color_hex(theme::SURFACE_2), LV_STATE_PRESSED);
|
||||||
} else {
|
} else {
|
||||||
lv_obj_remove_flag(b, LV_OBJ_FLAG_CLICKABLE);
|
lv_obj_remove_flag(b, LV_OBJ_FLAG_CLICKABLE);
|
||||||
@@ -2877,7 +2916,7 @@ static void bubble(lv_obj_t* list, const char* from, const char* text, bool own,
|
|||||||
lv_obj_t* hdr = flexBox(b, LV_FLEX_FLOW_ROW);
|
lv_obj_t* hdr = flexBox(b, LV_FLEX_FLOW_ROW);
|
||||||
lv_obj_set_style_pad_column(hdr, 8, 0);
|
lv_obj_set_style_pad_column(hdr, 8, 0);
|
||||||
label(hdr, from, THEME_FONT_SMALL, theme::ACCENT); // names are <= 31 chars: fits the bubble
|
label(hdr, from, THEME_FONT_SMALL, theme::ACCENT); // names are <= 31 chars: fits the bubble
|
||||||
if (meta_in_header) label(hdr, meta, THEME_FONT_SMALL, theme::TEXT_MUTED);
|
if (meta_in_header) *age_out = label(hdr, meta, THEME_FONT_SMALL, theme::TEXT_MUTED);
|
||||||
}
|
}
|
||||||
bool mentions_me;
|
bool mentions_me;
|
||||||
msgText(b, text, own, 238, &mentions_me);
|
msgText(b, text, own, 238, &mentions_me);
|
||||||
@@ -2886,19 +2925,19 @@ static void bubble(lv_obj_t* list, const char* from, const char* text, bool own,
|
|||||||
lv_obj_set_style_border_color(b, lv_color_hex(theme::ACCENT), 0);
|
lv_obj_set_style_border_color(b, lv_color_hex(theme::ACCENT), 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (loc_idx >= 0) { // position: navigate there / keep it as a waypoint
|
if (loc) { // position: navigate there / keep it as a waypoint
|
||||||
lv_obj_t* acts = flexBox(b, LV_FLEX_FLOW_ROW);
|
lv_obj_t* acts = flexBox(b, LV_FLEX_FLOW_ROW);
|
||||||
lv_obj_set_style_pad_column(acts, 6, 0);
|
lv_obj_set_style_pad_column(acts, 6, 0);
|
||||||
lv_obj_set_style_pad_top(acts, 2, 0);
|
lv_obj_set_style_pad_top(acts, 2, 0);
|
||||||
msgLocButton(acts, UI_SYMBOL_COMPASS " Go", loc_idx, false, true);
|
msgLocButton(acts, UI_SYMBOL_COMPASS " Go", false, true);
|
||||||
msgLocButton(acts, UI_SYMBOL_FLAG " Save", loc_idx, true, false);
|
msgLocButton(acts, UI_SYMBOL_FLAG " Save", true, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!meta_in_header) { // the age, then the delivery mark in its colour (as L1)
|
if (!meta_in_header) { // the age, then the delivery mark in its colour (as L1)
|
||||||
lv_obj_t* line = flexBox(b, LV_FLEX_FLOW_ROW);
|
lv_obj_t* line = flexBox(b, LV_FLEX_FLOW_ROW);
|
||||||
lv_obj_set_flex_align(line, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
|
lv_obj_set_flex_align(line, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
|
||||||
lv_obj_set_style_pad_column(line, 6, 0);
|
lv_obj_set_style_pad_column(line, 6, 0);
|
||||||
label(line, meta, THEME_FONT_SMALL, theme::TEXT_MUTED);
|
*age_out = label(line, meta, THEME_FONT_SMALL, theme::TEXT_MUTED);
|
||||||
if (relays > 0) { // as L1: the count alone says it got out, no check beside it
|
if (relays > 0) { // as L1: the count alone says it got out, no check beside it
|
||||||
lv_obj_t* c = lv_obj_create(line);
|
lv_obj_t* c = lv_obj_create(line);
|
||||||
lv_obj_remove_style_all(c);
|
lv_obj_remove_style_all(c);
|
||||||
@@ -2916,16 +2955,16 @@ static void bubble(lv_obj_t* list, const char* from, const char* text, bool own,
|
|||||||
label(line, status, THEME_FONT_SMALL, status_col);
|
label(line, status, THEME_FONT_SMALL, status_col);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return row;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remember the position in `text` (if any) for its bubble's buttons; label
|
// Remember the position in `text` (if any) for its bubble's buttons, in
|
||||||
// from a [WAY] tag, else the sender. Returns the slot or -1.
|
// slot r; label from a [WAY] tag, else the sender. Returns r or -1.
|
||||||
static int noteMsgLocation(const char* text, const char* sender) {
|
static int noteMsgLocation(int r, const char* text, const char* sender) {
|
||||||
if (s_msg_loc_n >= THREAD_MAX_SHOWN) return -1;
|
MsgLoc& m = s_msg_locs[r];
|
||||||
MsgLoc& m = s_msg_locs[s_msg_loc_n];
|
|
||||||
if (!geo::parseLatLon(text, m.lat, m.lon, m.label, sizeof(m.label))) return -1;
|
if (!geo::parseLatLon(text, m.lat, m.lon, m.label, sizeof(m.label))) return -1;
|
||||||
if (!m.label[0]) snprintf(m.label, sizeof(m.label), "%s", sender && sender[0] ? sender : "Msg loc");
|
if (!m.label[0]) snprintf(m.label, sizeof(m.label), "%s", sender && sender[0] ? sender : "Msg loc");
|
||||||
return s_msg_loc_n++;
|
return r;
|
||||||
}
|
}
|
||||||
|
|
||||||
void UITask::messageLocationAction(int idx, bool save) {
|
void UITask::messageLocationAction(int idx, bool save) {
|
||||||
@@ -2956,11 +2995,12 @@ int UITask::loadThreadPage(int& total) {
|
|||||||
char path[64];
|
char path[64];
|
||||||
if (_thread_is_channel) {
|
if (_thread_is_channel) {
|
||||||
int ring = h.histCountForChannel(_thread_channel);
|
int ring = h.histCountForChannel(_thread_channel);
|
||||||
int arc = s_archive.ready() && s_archive.chPath(_thread_channel, path, sizeof(path))
|
int arc = 0;
|
||||||
? s_archive.total<ChHistEntry>(path) : 0;
|
int got = s_archive.ready() && s_archive.chPath(_thread_channel, path, sizeof(path))
|
||||||
|
? s_archive.window(path, _thread_skip, THREAD_MAX_SHOWN, s_th_ch, &arc) : 0;
|
||||||
if (arc > 0 && arc >= ring) {
|
if (arc > 0 && arc >= ring) {
|
||||||
total = arc;
|
total = arc;
|
||||||
return s_archive.window(path, _thread_skip, THREAD_MAX_SHOWN, s_th_ch);
|
return got;
|
||||||
}
|
}
|
||||||
total = ring;
|
total = ring;
|
||||||
int m = ring - _thread_skip;
|
int m = ring - _thread_skip;
|
||||||
@@ -2969,14 +3009,14 @@ int UITask::loadThreadPage(int& total) {
|
|||||||
return m > 0 ? m : 0;
|
return m > 0 ? m : 0;
|
||||||
}
|
}
|
||||||
int ring = h.dmHistCountForContact(_thread_key);
|
int ring = h.dmHistCountForContact(_thread_key);
|
||||||
int arc = 0;
|
int arc = 0, got = 0;
|
||||||
if (s_archive.ready()) {
|
if (s_archive.ready()) {
|
||||||
histstore::SdArchive::dmPath(_thread_key, path, sizeof(path));
|
histstore::SdArchive::dmPath(_thread_key, path, sizeof(path));
|
||||||
arc = s_archive.total<DmHistEntry>(path);
|
got = s_archive.window(path, _thread_skip, THREAD_MAX_SHOWN, s_th_dm, &arc);
|
||||||
}
|
}
|
||||||
if (arc > 0 && arc >= ring) {
|
if (arc > 0 && arc >= ring) {
|
||||||
total = arc;
|
total = arc;
|
||||||
return s_archive.window(path, _thread_skip, THREAD_MAX_SHOWN, s_th_dm);
|
return got;
|
||||||
}
|
}
|
||||||
total = ring;
|
total = ring;
|
||||||
int m = ring - _thread_skip;
|
int m = ring - _thread_skip;
|
||||||
@@ -3007,23 +3047,36 @@ void UITask::refreshThread() {
|
|||||||
if (!_thread_list) return;
|
if (!_thread_list) return;
|
||||||
_thread_dirty = false;
|
_thread_dirty = false;
|
||||||
_thread_sig = threadSignature();
|
_thread_sig = threadSignature();
|
||||||
lv_obj_clean(_thread_list);
|
|
||||||
const MessageHistory& h = _core->history;
|
const MessageHistory& h = _core->history;
|
||||||
s_msg_loc_n = 0;
|
|
||||||
s_msg_meta_n = 0;
|
|
||||||
int total = 0;
|
int total = 0;
|
||||||
int n = loadThreadPage(total);
|
int n = loadThreadPage(total);
|
||||||
if (n == 0 && _thread_skip > 0) { // the page went away (history trimmed)
|
if (n == 0 && _thread_skip > 0) { // the page went away (history trimmed)
|
||||||
_thread_skip = 0;
|
_thread_skip = 0;
|
||||||
n = loadThreadPage(total);
|
n = loadThreadPage(total);
|
||||||
} if (total > _thread_skip + n) {
|
|
||||||
char t[40];
|
|
||||||
snprintf(t, sizeof(t), LV_SYMBOL_UP " Older messages (%d)", total - _thread_skip - n);
|
|
||||||
pageButton(_thread_list, t, 1);
|
|
||||||
}
|
}
|
||||||
|
ContactInfo tc;
|
||||||
|
bool room = !_thread_is_channel && MessageHistory::contactByPrefix(_thread_key, tc) && tc.type == ADV_TYPE_ROOM;
|
||||||
|
|
||||||
if (_thread_is_channel) {
|
// Which message entry i is, and the delivery state its bubble shows.
|
||||||
for (int i = 0; i < n; i++) { // oldest first
|
auto keyOf = [&](int i) {
|
||||||
|
uint32_t ts = _thread_is_channel ? s_th_ch[i].timestamp : s_th_dm[i].timestamp;
|
||||||
|
const char* t = _thread_is_channel ? s_th_ch[i].text : s_th_dm[i].text;
|
||||||
|
uint32_t k = 2166136261u ^ ts;
|
||||||
|
for (; *t; t++) k = (k ^ (uint8_t)*t) * 16777619u;
|
||||||
|
return k;
|
||||||
|
};
|
||||||
|
auto stateOf = [&](int i) -> uint32_t {
|
||||||
|
if (_thread_is_channel) return s_th_ch[i].relay_status | (uint32_t)s_th_ch[i].path_len << 8;
|
||||||
|
return s_th_dm[i].outgoing ? 0x100u | h.dmEffectiveStatus(s_th_dm[i]) : 0;
|
||||||
|
};
|
||||||
|
// Entry i's bubble (at the list's end) and its menu / position slots.
|
||||||
|
auto build = [&](int i) {
|
||||||
|
MsgMeta& m = s_msg_meta[i];
|
||||||
|
const char* st = NULL; uint32_t col = theme::TEXT_MUTED;
|
||||||
|
int nrel = 0;
|
||||||
|
lv_obj_t* age = nullptr;
|
||||||
|
lv_obj_t* row;
|
||||||
|
if (_thread_is_channel) {
|
||||||
const ChHistEntry& e = s_th_ch[i];
|
const ChHistEntry& e = s_th_ch[i];
|
||||||
// Channel text is "Sender: body"; our own posts are filed as "Me: body".
|
// Channel text is "Sender: body"; our own posts are filed as "Me: body".
|
||||||
char from[40] = "";
|
char from[40] = "";
|
||||||
@@ -3038,22 +3091,16 @@ void UITask::refreshThread() {
|
|||||||
// Own posts: once a repeater echoed it, how many distinct repeaters did
|
// Own posts: once a repeater echoed it, how many distinct repeaters did
|
||||||
// (markChannelRelayed), or a check if that's unknown; nothing before --
|
// (markChannelRelayed), or a check if that's unknown; nothing before --
|
||||||
// no echo is normal.
|
// no echo is normal.
|
||||||
const char* st = NULL; uint32_t col = theme::TEXT_MUTED;
|
|
||||||
int nrel = 0;
|
|
||||||
if (own && e.relay_status == ACK_OK) {
|
if (own && e.relay_status == ACK_OK) {
|
||||||
nrel = e.path_len & 63;
|
nrel = e.path_len & 63;
|
||||||
st = LV_SYMBOL_OK; col = theme::OK;
|
st = LV_SYMBOL_OK; col = theme::OK;
|
||||||
} else if (own) st = "";
|
} else if (own) st = "";
|
||||||
int loc = own ? -1 : noteMsgLocation(body, from);
|
int loc = own ? -1 : noteMsgLocation(i, body, from);
|
||||||
bubble(_thread_list, own ? NULL : from, body, own, e.timestamp, st, col,
|
m = { i, true, own, loc, "" };
|
||||||
loc, noteMsgMeta(i, true, own, loc, own ? "" : from), nrel);
|
if (!own) snprintf(m.from, sizeof(m.from), "%s", from);
|
||||||
}
|
row = bubble(_thread_list, own ? NULL : from, body, own, e.timestamp, st, col, loc >= 0, true, nrel, &age);
|
||||||
} else {
|
} else {
|
||||||
ContactInfo tc;
|
|
||||||
bool room = MessageHistory::contactByPrefix(_thread_key, tc) && tc.type == ADV_TYPE_ROOM;
|
|
||||||
for (int i = 0; i < n; i++) {
|
|
||||||
const DmHistEntry& e = s_th_dm[i];
|
const DmHistEntry& e = s_th_dm[i];
|
||||||
const char* st = NULL; uint32_t col = theme::TEXT_MUTED;
|
|
||||||
if (e.outgoing) {
|
if (e.outgoing) {
|
||||||
switch (h.dmEffectiveStatus(e)) {
|
switch (h.dmEffectiveStatus(e)) {
|
||||||
case ACK_OK: st = LV_SYMBOL_OK; col = theme::OK; break;
|
case ACK_OK: st = LV_SYMBOL_OK; col = theme::OK; break;
|
||||||
@@ -3067,11 +3114,71 @@ void UITask::refreshThread() {
|
|||||||
const char* text = e.text;
|
const char* text = e.text;
|
||||||
if (!e.outgoing && room) text = contactctl::splitRoomPost(e.text, who, sizeof(who));
|
if (!e.outgoing && room) text = contactctl::splitRoomPost(e.text, who, sizeof(who));
|
||||||
else if (!e.outgoing) contactName(_thread_key, who, sizeof(who));
|
else if (!e.outgoing) contactName(_thread_key, who, sizeof(who));
|
||||||
int loc = e.outgoing ? -1 : noteMsgLocation(text, who);
|
int loc = e.outgoing ? -1 : noteMsgLocation(i, text, who);
|
||||||
bubble(_thread_list, room && !e.outgoing ? who : NULL, text, e.outgoing, e.timestamp, st, col,
|
m = { i, false, false, loc, "" };
|
||||||
loc, e.outgoing ? -1 : noteMsgMeta(i, false, false, loc, who)); // nothing to show for our own DM
|
snprintf(m.from, sizeof(m.from), "%s", who);
|
||||||
|
row = bubble(_thread_list, room && !e.outgoing ? who : NULL, text, e.outgoing, e.timestamp, st, col,
|
||||||
|
loc >= 0, !e.outgoing, 0, &age); // nothing to show for our own DM
|
||||||
}
|
}
|
||||||
|
s_th_rows[i] = { keyOf(i), stateOf(i), row, age };
|
||||||
|
};
|
||||||
|
auto olderText = [&](char* t, int sz) { snprintf(t, sz, LV_SYMBOL_UP " Older messages (%d)", total - _thread_skip - n); };
|
||||||
|
|
||||||
|
// The same page as built, with messages added at the end (the oldest
|
||||||
|
// dropping off the top) and delivery marks changed: patch it.
|
||||||
|
int kept = -1; // bubbles still shown, after `drop` gone from the top
|
||||||
|
int drop = 0;
|
||||||
|
if (s_th_rows_newest && s_th_rows_n > 0 && n > 0 && _thread_skip == 0 && (total > n) == (s_older_lbl != nullptr)) {
|
||||||
|
for (drop = 0; drop < s_th_rows_n && kept < 0; drop++) {
|
||||||
|
int m = s_th_rows_n - drop;
|
||||||
|
if (m > n) continue;
|
||||||
|
bool same = true;
|
||||||
|
for (int r = 0; r < m && same; r++) same = s_th_rows[drop + r].key == keyOf(r);
|
||||||
|
if (same) kept = m;
|
||||||
|
}
|
||||||
|
drop--;
|
||||||
}
|
}
|
||||||
|
if (kept > 0) {
|
||||||
|
for (int r = 0; r < drop; r++) lv_obj_delete(s_th_rows[r].row);
|
||||||
|
memmove(s_th_rows, s_th_rows + drop, kept * sizeof(ThreadRow));
|
||||||
|
memmove(s_msg_meta, s_msg_meta + drop, kept * sizeof(MsgMeta));
|
||||||
|
for (int r = 0; r < kept; r++) {
|
||||||
|
if (s_th_rows[r].state == stateOf(r)) { // unchanged: its slots, and a fresh age
|
||||||
|
const ChHistEntry* ce = _thread_is_channel ? &s_th_ch[r] : nullptr;
|
||||||
|
char meta[32];
|
||||||
|
fmtMsgAge(meta, sizeof(meta), rtc_clock.getCurrentTime(), ce ? ce->timestamp : s_th_dm[r].timestamp, _prefs);
|
||||||
|
if (s_th_rows[r].age) setText(s_th_rows[r].age, meta);
|
||||||
|
s_msg_meta[r].pos = r;
|
||||||
|
if (s_msg_meta[r].loc >= 0 && drop > 0) { s_msg_locs[r] = s_msg_locs[r + drop]; s_msg_meta[r].loc = r; }
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
lv_obj_t* old = s_th_rows[r].row; // a delivery mark changed: that bubble anew, in its place
|
||||||
|
build(r);
|
||||||
|
lv_obj_move_to_index(s_th_rows[r].row, lv_obj_get_index(old));
|
||||||
|
lv_obj_delete(old);
|
||||||
|
}
|
||||||
|
for (int r = kept; r < n; r++) build(r);
|
||||||
|
if (s_older_lbl) { char t[40]; olderText(t, sizeof(t)); setText(s_older_lbl, t); }
|
||||||
|
s_th_rows_n = s_msg_meta_n = s_msg_loc_n = n;
|
||||||
|
if (n > kept) { // new messages: down to them
|
||||||
|
lv_obj_update_layout(_thread_list);
|
||||||
|
lv_obj_scroll_to_y(_thread_list, LV_COORD_MAX, LV_ANIM_OFF);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
lv_obj_clean(_thread_list);
|
||||||
|
s_th_rows_n = 0;
|
||||||
|
s_older_lbl = nullptr;
|
||||||
|
if (total > _thread_skip + n) {
|
||||||
|
char t[40];
|
||||||
|
olderText(t, sizeof(t));
|
||||||
|
pageButton(_thread_list, t, 1);
|
||||||
|
s_older_lbl = lv_obj_get_child(lv_obj_get_child(_thread_list, -1), 0);
|
||||||
|
}
|
||||||
|
for (int i = 0; i < n; i++) build(i); // oldest first
|
||||||
|
s_th_rows_n = s_msg_meta_n = s_msg_loc_n = n;
|
||||||
|
s_th_rows_newest = _thread_skip == 0; // a page back is built whole each time
|
||||||
if (_thread_skip > 0) pageButton(_thread_list, LV_SYMBOL_DOWN " Newer messages", -1);
|
if (_thread_skip > 0) pageButton(_thread_list, LV_SYMBOL_DOWN " Newer messages", -1);
|
||||||
if (n == 0) label(_thread_list, "No messages yet", THEME_FONT_BODY, theme::TEXT_MUTED);
|
if (n == 0) label(_thread_list, "No messages yet", THEME_FONT_BODY, theme::TEXT_MUTED);
|
||||||
lv_obj_update_layout(_thread_list);
|
lv_obj_update_layout(_thread_list);
|
||||||
@@ -3521,6 +3628,12 @@ extern "C" EMSCRIPTEN_KEEPALIVE void sim_open(const char* name) {
|
|||||||
if (!strcmp(name, "regions")) { s_ui->mapRegionsPopup(); return; }
|
if (!strcmp(name, "regions")) { s_ui->mapRegionsPopup(); return; }
|
||||||
if (!strcmp(name, "region0")) { s_ui->mapRegionPopup(0); return; }
|
if (!strcmp(name, "region0")) { s_ui->mapRegionPopup(0); return; }
|
||||||
if (!strcmp(name, "advert")) { s_ui->advertPopup(); return; }
|
if (!strcmp(name, "advert")) { s_ui->advertPopup(); return; }
|
||||||
|
if (!strncmp(name, "trail@", 6)) { // "trail@name.trl" from /sdcard/trails: Load, as the saved-trail popup does
|
||||||
|
navmap::scanTrails();
|
||||||
|
for (int i = 0; i < navmap::s_st_n; i++)
|
||||||
|
if (!strcmp(navmap::s_st_names[i], name + 6)) { navmap::s_st_sel = i; s_ui->savedTrailAction(navmap::TL_ST_LOAD); }
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// Scrolls the screen's (or a popup's) main list by dy; returns what was left to scroll.
|
// Scrolls the screen's (or a popup's) main list by dy; returns what was left to scroll.
|
||||||
static void dbgScrollable(lv_obj_t* o, lv_obj_t*& best, int& h) {
|
static void dbgScrollable(lv_obj_t* o, lv_obj_t*& best, int& h) {
|
||||||
|
|||||||
@@ -312,7 +312,7 @@ private:
|
|||||||
void buildHomeClock(lv_obj_t* box);
|
void buildHomeClock(lv_obj_t* box);
|
||||||
void buildHomeMap(lv_obj_t* box);
|
void buildHomeMap(lv_obj_t* box);
|
||||||
void homeFieldText(uint8_t field, char* buf, int n);
|
void homeFieldText(uint8_t field, char* buf, int n);
|
||||||
void homeMapFit();
|
bool homeMapFit();
|
||||||
void homeMapLayout();
|
void homeMapLayout();
|
||||||
void homeMapLoop();
|
void homeMapLoop();
|
||||||
void refreshHome();
|
void refreshHome();
|
||||||
@@ -349,6 +349,9 @@ private:
|
|||||||
void buildNavLayers();
|
void buildNavLayers();
|
||||||
void buildNavControls(lv_obj_t* body);
|
void buildNavControls(lv_obj_t* body);
|
||||||
void rebuildNavMarkers();
|
void rebuildNavMarkers();
|
||||||
|
bool navTrailSync();
|
||||||
|
bool trailJournalTick(bool now = false); // NavMap.h: the live trail's copy on the card
|
||||||
|
void trailJournalRestore();
|
||||||
void layoutNav();
|
void layoutNav();
|
||||||
void refreshNavBar();
|
void refreshNavBar();
|
||||||
void navFrameTarget();
|
void navFrameTarget();
|
||||||
@@ -534,6 +537,8 @@ private:
|
|||||||
lv_obj_t* _map_marks = nullptr;
|
lv_obj_t* _map_marks = nullptr;
|
||||||
lv_obj_t* _map_me = nullptr;
|
lv_obj_t* _map_me = nullptr;
|
||||||
lv_obj_t* _map_zoom_lbl = nullptr;
|
lv_obj_t* _map_zoom_lbl = nullptr;
|
||||||
|
lv_obj_t* _map_center_btn = nullptr; // accent while the map follows you
|
||||||
|
uint32_t _next_follow_ms = 0;
|
||||||
lv_obj_t* _map_hint = nullptr;
|
lv_obj_t* _map_hint = nullptr;
|
||||||
lv_obj_t* _map_dl_pill = nullptr; // download progress over the map
|
lv_obj_t* _map_dl_pill = nullptr; // download progress over the map
|
||||||
lv_obj_t* _dl_overlay = nullptr; // download popup
|
lv_obj_t* _dl_overlay = nullptr; // download popup
|
||||||
|
|||||||
@@ -57,6 +57,13 @@ static void drawTrail(DisplayDriver& d, TrailStore& tr, Project project, OnBreak
|
|||||||
else drawLine(d, x0, y0, x1, y1);
|
else drawLine(d, x0, y0, x1, y1);
|
||||||
x0 = x1; y0 = y1;
|
x0 = x1; y0 = y1;
|
||||||
}
|
}
|
||||||
|
// The simplifier's pending point: the straight stretch walked since the
|
||||||
|
// last vertex, so the line reaches where you are instead of lagging behind.
|
||||||
|
TrailPoint pend;
|
||||||
|
if (tr.pendingPoint(pend)) {
|
||||||
|
int x1, y1; project(pend.lat_1e6, pend.lon_1e6, x1, y1);
|
||||||
|
drawLine(d, x0, y0, x1, y1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace gfx
|
} // namespace gfx
|
||||||
|
|||||||
@@ -69,7 +69,9 @@ build_flags =
|
|||||||
-D OTA_ASSET_SUFFIX='"-Wio-Tracker-L2-ota.bin"' ; Settings > Firmware update
|
-D OTA_ASSET_SUFFIX='"-Wio-Tracker-L2-ota.bin"' ; Settings > Firmware update
|
||||||
; Sized for the PSRAM (UiCore is heap-allocated, so these land there);
|
; Sized for the PSRAM (UiCore is heap-allocated, so these land there);
|
||||||
; the defaults in the headers are the nRF52 boards'.
|
; the defaults in the headers are the nRF52 boards'.
|
||||||
-D TRAIL_CAPACITY=4096 ; 64 KB (+64 KB of map drawing buffers)
|
-D TRAIL_CAPACITY=32768 ; 512 KB (+512 KB of map drawing buffers): ~160 km at 5 m
|
||||||
|
-D TRAIL_SIMPLIFY=0 ; every point kept: no straight-run simplification
|
||||||
|
-D TRAIL_FIXED_MIN_DELTA_M=5 ; a fixed GPS-jitter gate instead of the point spacing setting
|
||||||
-D WAYPOINT_CAPACITY=64
|
-D WAYPOINT_CAPACITY=64
|
||||||
-D HIST_CH_MAX=256 ; all channels together, ~50 KB
|
-D HIST_CH_MAX=256 ; all channels together, ~50 KB
|
||||||
-D HIST_DM_MAX=128 ; all DMs together, ~27 KB
|
-D HIST_DM_MAX=128 ; all DMs together, ~27 KB
|
||||||
|
|||||||
@@ -187,7 +187,7 @@ if [ "$SIM_UI" = "lvgl" ]; then
|
|||||||
done
|
done
|
||||||
# The L2's PSRAM-sized limits (solo/wio-tracker-l2/platformio.ini).
|
# The L2's PSRAM-sized limits (solo/wio-tracker-l2/platformio.ini).
|
||||||
DEFINES=("${kept[@]}" -DDISPLAY_CLASS=SimLcdDisplay -DMAX_GROUP_CHANNELS=40 -DUI_ZOOM=1 -DLV_CONF_INCLUDE_SIMPLE
|
DEFINES=("${kept[@]}" -DDISPLAY_CLASS=SimLcdDisplay -DMAX_GROUP_CHANNELS=40 -DUI_ZOOM=1 -DLV_CONF_INCLUDE_SIMPLE
|
||||||
-DTRAIL_CAPACITY=4096 -DWAYPOINT_CAPACITY=64 -DHIST_CH_MAX=256 -DHIST_DM_MAX=128 -DNEARBY_MAX=64 -DHIST_ARCHIVE)
|
-DTRAIL_CAPACITY=32768 -DTRAIL_SIMPLIFY=0 -DTRAIL_FIXED_MIN_DELTA_M=5 -DWAYPOINT_CAPACITY=64 -DHIST_CH_MAX=256 -DHIST_DM_MAX=128 -DNEARBY_MAX=64 -DHIST_ARCHIVE)
|
||||||
INCLUDES+=("-I$LVGL_DIR")
|
INCLUDES+=("-I$LVGL_DIR")
|
||||||
LINK_EXTRA=(-sFETCH=1) # map downloads (LvglPort.h) go through emscripten_fetch
|
LINK_EXTRA=(-sFETCH=1) # map downloads (LvglPort.h) go through emscripten_fetch
|
||||||
fi
|
fi
|
||||||
|
|||||||
Reference in New Issue
Block a user