mirror of
https://github.com/MarekZegare4/MeshCore-Solo.git
synced 2026-11-01 02:11:57 +00:00
feat(ui-core): status history, trail profile and calendar math, shared
The history of the noise floor, battery, traffic and SNR, a trail's height profile (with a sampler for trails read point by point off the card) and a month's calendar, moved out of ui-new so ui-lvgl can draw the same. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,44 @@
|
||||
#pragma once
|
||||
// A month's calendar, shared by both UIs: its length, the column (0 = Monday)
|
||||
// its 1st falls in, how many weeks it spans, and the days left in it the
|
||||
// alarm goes off on. ui-new draws it on e-ink (CalendarView.h), ui-lvgl in
|
||||
// Clock > Calendar.
|
||||
|
||||
#include <stdint.h>
|
||||
#include <time.h>
|
||||
|
||||
namespace calmath {
|
||||
|
||||
inline int daysIn(int year, int mon) { // mon 0..11, year as in tm (since 1900)
|
||||
static const uint8_t D[] = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
|
||||
const int y = year + 1900;
|
||||
const bool leap = (y % 4 == 0 && y % 100 != 0) || y % 400 == 0;
|
||||
return D[mon] + (mon == 1 && leap ? 1 : 0);
|
||||
}
|
||||
// The column (0 = Monday) the 1st falls in.
|
||||
inline int firstCol(const struct tm& t) {
|
||||
const int wday1 = ((t.tm_wday - (t.tm_mday - 1)) % 7 + 7) % 7; // 0 = Sunday
|
||||
return (wday1 + 6) % 7;
|
||||
}
|
||||
inline int weeks(const struct tm& t) { return (firstCol(t) + daysIn(t.tm_year, t.tm_mon) + 6) / 7; }
|
||||
|
||||
// The days left this month the alarm goes off on, bit (day - 1) each: the
|
||||
// repeat days (`mask`: bit per tm_wday), or a one-shot's next (today or
|
||||
// tomorrow). `t` is the local time now.
|
||||
inline uint32_t alarmDays(const struct tm& t, bool on, uint8_t mask, int hour, int min) {
|
||||
if (!on) return 0;
|
||||
const int n = daysIn(t.tm_year, t.tm_mon);
|
||||
const bool passed = t.tm_hour * 60 + t.tm_min >= hour * 60 + min;
|
||||
uint32_t days = 0;
|
||||
for (int day = t.tm_mday; day <= n; day++) {
|
||||
if (day == t.tm_mday && passed) continue;
|
||||
const int wday = (t.tm_wday + day - t.tm_mday) % 7;
|
||||
if (mask ? (mask & (1 << wday)) : true) {
|
||||
days |= 1UL << (day - 1);
|
||||
if (!mask) break; // one-shot: only the next
|
||||
}
|
||||
}
|
||||
return days;
|
||||
}
|
||||
|
||||
} // namespace calmath
|
||||
@@ -0,0 +1,53 @@
|
||||
#pragma once
|
||||
// The Status history, shared by both UIs: the noise floor, the battery and
|
||||
// the mesh traffic, one sample every SAMPLE_MS into a ring of N, and the SNR
|
||||
// of each packet heard (the last 32). ui-new draws it on its Status tabs,
|
||||
// ui-lvgl in Diagnostics > History. The UI loop calls tick() on every pass
|
||||
// with the readings; it keeps its own pace.
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
namespace uicore {
|
||||
|
||||
// A ring of the last N samples, oldest first.
|
||||
template <int N>
|
||||
struct History {
|
||||
int16_t v[N];
|
||||
uint8_t n = 0, head = 0;
|
||||
void push(int16_t x) { v[head] = x; head = (head + 1) % N; if (n < N) n++; }
|
||||
int16_t at(int i) const { return v[(head + N - n + i) % N]; } // 0 = oldest
|
||||
static constexpr int size() { return N; }
|
||||
};
|
||||
|
||||
template <int N, unsigned long SAMPLE_MS>
|
||||
struct StatusHistory {
|
||||
static constexpr int SAMPLES = N;
|
||||
static constexpr unsigned long INTERVAL_MS = SAMPLE_MS;
|
||||
|
||||
History<N> noise; // dBm
|
||||
History<N> batt; // mV
|
||||
History<N> traffic; // packets received in each interval
|
||||
History<32> snr; // dB x4, a sample a packet
|
||||
uint32_t version = 0; // counts every change, for a view to see one
|
||||
|
||||
// `noise_dbm` 0 = no reading (the sample is skipped); `total_rx` counts
|
||||
// every packet received since boot; `last_snr` is the last packet's.
|
||||
void tick(unsigned long now, int noise_dbm, int batt_mv, uint32_t total_rx, float last_snr) {
|
||||
if (total_rx != _snr_rx) { snr.push((int16_t)(last_snr * 4)); _snr_rx = total_rx; version++; }
|
||||
if (_next && (long)(now - _next) < 0) return;
|
||||
_next = now + SAMPLE_MS;
|
||||
if (noise_dbm) noise.push((int16_t)noise_dbm);
|
||||
batt.push((int16_t)batt_mv);
|
||||
if (_last_rx || total_rx) traffic.push((int16_t)(total_rx - _last_rx > 30000 ? 30000 : total_rx - _last_rx));
|
||||
_last_rx = total_rx;
|
||||
version++;
|
||||
}
|
||||
// After the packet counters are reset: the next interval counts from 0.
|
||||
void countersReset() { _last_rx = 0; _snr_rx = 0; }
|
||||
|
||||
private:
|
||||
unsigned long _next = 0;
|
||||
uint32_t _last_rx = 0, _snr_rx = 0;
|
||||
};
|
||||
|
||||
} // namespace uicore
|
||||
@@ -0,0 +1,85 @@
|
||||
#pragma once
|
||||
// A trail's height profile, shared by both UIs: the lowest and highest
|
||||
// altitude, the climb (rises past 3 m of GPS jitter), and a walk over the
|
||||
// points that carry an altitude with the distance covered to each. Duck-typed
|
||||
// over any store with count() / at(i) whose points have lat_1e6, lon_1e6,
|
||||
// flags and alt_m (TrailStore, a saved trail).
|
||||
|
||||
#include "../Trail.h"
|
||||
|
||||
namespace trailprofile {
|
||||
|
||||
struct Stats {
|
||||
bool any = false; // some point has an altitude
|
||||
int lo = 0, hi = 0; // metres
|
||||
int gain = 0; // the climb, metres
|
||||
// One altitude more, in order.
|
||||
void add(int a) {
|
||||
if (!any) { lo = hi = a; any = true; }
|
||||
if (a < lo) lo = a;
|
||||
if (a > hi) hi = a;
|
||||
if (_last == TRAIL_ALT_NONE || a < _last) _last = a;
|
||||
else if (a - _last >= 3) { gain += a - _last; _last = a; }
|
||||
}
|
||||
private:
|
||||
int _last = TRAIL_ALT_NONE;
|
||||
};
|
||||
|
||||
template <class Store>
|
||||
Stats stats(const Store& s) {
|
||||
Stats st;
|
||||
for (int i = 0; i < s.count(); i++)
|
||||
if (s.at(i).alt_m != TRAIL_ALT_NONE) st.add(s.at(i).alt_m);
|
||||
return st;
|
||||
}
|
||||
|
||||
// fn(metres from the start, altitude) for each point with an altitude, in
|
||||
// order. A segment start adds no distance (a gap in the recording).
|
||||
template <class Store, class Fn>
|
||||
void walk(const Store& s, Fn fn) {
|
||||
float cum = 0;
|
||||
for (int i = 0; i < s.count(); i++) {
|
||||
const auto& p = s.at(i);
|
||||
if (i > 0 && !(p.flags & TRAIL_FLAG_SEG_START))
|
||||
cum += TrailStore::haversineMeters(s.at(i - 1).lat_1e6, s.at(i - 1).lon_1e6, p.lat_1e6, p.lon_1e6);
|
||||
if (p.alt_m != TRAIL_ALT_NONE) fn(cum, (int)p.alt_m);
|
||||
}
|
||||
}
|
||||
|
||||
// The profile of a trail too long to keep (a file read point by point): at
|
||||
// most K samples, the stats of all of them. Fed point by point; when full,
|
||||
// every other sample goes and only every second one after is kept, so the
|
||||
// samples stay spread over the whole trail.
|
||||
struct Sampler {
|
||||
static const int K = 128;
|
||||
float dist[K]; // metres from the start
|
||||
int16_t alt[K];
|
||||
int n = 0;
|
||||
float total = 0; // the trail's length, metres
|
||||
Stats st;
|
||||
|
||||
void feed(const TrailPoint& p) {
|
||||
if (_have && !(p.flags & TRAIL_FLAG_SEG_START))
|
||||
total += TrailStore::haversineMeters(_prev_lat, _prev_lon, p.lat_1e6, p.lon_1e6);
|
||||
_have = true; _prev_lat = p.lat_1e6; _prev_lon = p.lon_1e6;
|
||||
if (p.alt_m == TRAIL_ALT_NONE) return;
|
||||
st.add(p.alt_m);
|
||||
if (_skip++ % _stride) return;
|
||||
if (n == K) { // full: keep every other, take every second from now on
|
||||
for (int i = 0; i < K / 2; i++) { dist[i] = dist[2 * i]; alt[i] = alt[2 * i]; }
|
||||
n = K / 2;
|
||||
_stride *= 2;
|
||||
_skip = 1;
|
||||
}
|
||||
dist[n] = total; alt[n] = p.alt_m; n++;
|
||||
}
|
||||
template <class Store>
|
||||
void feedAll(const Store& s) { for (int i = 0; i < s.count(); i++) feed(s.at(i)); }
|
||||
|
||||
private:
|
||||
bool _have = false;
|
||||
int32_t _prev_lat = 0, _prev_lon = 0;
|
||||
int _stride = 1, _skip = 0;
|
||||
};
|
||||
|
||||
} // namespace trailprofile
|
||||
Reference in New Issue
Block a user