diff --git a/examples/companion_radio/ui-core/CalendarMath.h b/examples/companion_radio/ui-core/CalendarMath.h new file mode 100644 index 00000000..b4a26de6 --- /dev/null +++ b/examples/companion_radio/ui-core/CalendarMath.h @@ -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 +#include + +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 diff --git a/examples/companion_radio/ui-core/StatusHistory.h b/examples/companion_radio/ui-core/StatusHistory.h new file mode 100644 index 00000000..8ab20bd2 --- /dev/null +++ b/examples/companion_radio/ui-core/StatusHistory.h @@ -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 + +namespace uicore { + +// A ring of the last N samples, oldest first. +template +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 +struct StatusHistory { + static constexpr int SAMPLES = N; + static constexpr unsigned long INTERVAL_MS = SAMPLE_MS; + + History noise; // dBm + History batt; // mV + History 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 diff --git a/examples/companion_radio/ui-core/TrailProfile.h b/examples/companion_radio/ui-core/TrailProfile.h new file mode 100644 index 00000000..9b7f1003 --- /dev/null +++ b/examples/companion_radio/ui-core/TrailProfile.h @@ -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 +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 +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 + 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