mirror of
https://github.com/MarekZegare4/MeshCore-Solo.git
synced 2026-11-01 10:21:59 +00:00
feat(ui-new): e-ink graphics on the OLED, a Trail height view
The OLED gets the arrows towards nodes in Nodes and the Path row with its hop rings in a stored node's detail (its position on one line to make the room); no compass rose. Trail gets a height-profile view on both. The Status history, trail profile and calendar come from ui-core. A sim hook turns on the advert's position for tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -554,6 +554,17 @@ extern "C" EMSCRIPTEN_KEEPALIVE int sim_test_set_node_name(const char* name) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Whether adverts carry this node's position (ADVERT_LOC_NONE / _SHARE /
|
||||
// _PREFS, src/helpers/CommonCLI.h) -- off by default, so peers in a test
|
||||
// mesh have no distance or direction to one another until it's on.
|
||||
extern "C" EMSCRIPTEN_KEEPALIVE int sim_test_set_advert_loc(int policy) {
|
||||
if (!g_sim_ready) return 0;
|
||||
NodePrefs* prefs = the_mesh.getNodePrefs();
|
||||
if (!prefs) return 0;
|
||||
prefs->advert_loc_policy = (uint8_t)policy;
|
||||
return 1;
|
||||
}
|
||||
|
||||
// auto_off_secs (NodePrefs.h) defaults to 15 -- a real device's screen
|
||||
// (and, if auto_lock is also set, the UI itself) turns off/locks 15s
|
||||
// after the last input, real power-saving behaviour a battery-powered
|
||||
|
||||
@@ -6,21 +6,13 @@
|
||||
#include <time.h>
|
||||
#include <helpers/ui/DisplayDriver.h>
|
||||
#include "InfoKit.h"
|
||||
#include "../ui-core/CalendarMath.h"
|
||||
|
||||
namespace calendar {
|
||||
|
||||
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; }
|
||||
using calmath::daysIn;
|
||||
using calmath::firstCol;
|
||||
using calmath::weeks;
|
||||
|
||||
// Its height: the day names, a rule, then a row a week.
|
||||
inline int height(DisplayDriver& d, const struct tm& t) {
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include <string.h>
|
||||
#include <math.h>
|
||||
#include "icons.h"
|
||||
#include "../ui-core/StatusHistory.h"
|
||||
|
||||
namespace info {
|
||||
|
||||
@@ -249,12 +250,7 @@ inline void switchRow(DisplayDriver& d, int y, const char* label, bool on, bool
|
||||
|
||||
// The last N samples of one reading, oldest first, for a history line.
|
||||
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
|
||||
};
|
||||
using History = uicore::History<N>;
|
||||
|
||||
// A history line in a w x h box: a dotted baseline, the samples scaled
|
||||
// between their own min and max (at least `min_span` apart, so a flat
|
||||
|
||||
@@ -440,10 +440,19 @@ class NearbyScreen : public UIScreen, protected NearbyModel {
|
||||
int step = display.lineStep();
|
||||
if (step * 5 > display.height() - hdr) step = (display.height() - hdr) / 5;
|
||||
char buf[32];
|
||||
snprintf(buf, sizeof(buf), "Lat: %.5f", e.lat_e6 / 1e6);
|
||||
display.setCursor(2, hdr); display.print(buf);
|
||||
snprintf(buf, sizeof(buf), "Lon: %.5f", e.lon_e6 / 1e6);
|
||||
display.setCursor(2, hdr + step); display.print(buf);
|
||||
// Without a line to spare for the path (the OLED), the position takes one
|
||||
// row, unlabelled: then there are five rows either way.
|
||||
const bool one_pos = hdr + step * 5 + display.getLineHeight() > display.height();
|
||||
if (one_pos) {
|
||||
snprintf(buf, sizeof(buf), "%.5f,%.5f", e.lat_e6 / 1e6, e.lon_e6 / 1e6);
|
||||
display.drawTextEllipsized(2, hdr, display.width() - 4, buf);
|
||||
} else {
|
||||
snprintf(buf, sizeof(buf), "Lat: %.5f", e.lat_e6 / 1e6);
|
||||
display.setCursor(2, hdr); display.print(buf);
|
||||
snprintf(buf, sizeof(buf), "Lon: %.5f", e.lon_e6 / 1e6);
|
||||
display.setCursor(2, hdr + step); display.print(buf);
|
||||
}
|
||||
const int r0 = one_pos ? -1 : 0; // the rows under the position move up one
|
||||
|
||||
if (e.dist_km >= 0.0f) {
|
||||
char dist[12];
|
||||
@@ -453,32 +462,35 @@ class NearbyScreen : public UIScreen, protected NearbyModel {
|
||||
} else {
|
||||
snprintf(buf, sizeof(buf), "Dist: no GPS");
|
||||
}
|
||||
display.setCursor(2, hdr + step * 2); display.print(buf);
|
||||
display.setCursor(2, hdr + step * (2 + r0)); display.print(buf);
|
||||
snprintf(buf, sizeof(buf), "Type: %s", typeName(e.type));
|
||||
display.setCursor(2, hdr + step * 3); display.print(buf);
|
||||
display.setCursor(2, hdr + step * (3 + r0)); display.print(buf);
|
||||
char age[16];
|
||||
fmtAge(age, sizeof(age), e.lastmod);
|
||||
// For a live [LOC] row, label the timestamp as a position share and note
|
||||
// whether the sender's identity is verified (DM) or name-only (channel).
|
||||
if (e.is_live) snprintf(buf, sizeof(buf), "Sharing pos: %s %s", age, e.live_verified ? "(DM)" : "(chan)");
|
||||
else snprintf(buf, sizeof(buf), "Seen: %s", age);
|
||||
display.drawTextEllipsized(2, hdr + step * 4, display.width() - 4, buf);
|
||||
display.drawTextEllipsized(2, hdr + step * (4 + r0), display.width() - 4, buf);
|
||||
|
||||
const int W = display.width(), H = display.height(), lh = display.getLineHeight();
|
||||
int rows = 5;
|
||||
// How a message gets there, where there's a line for it (e-ink): a chain
|
||||
// from you to it, a ring per repeater on the way.
|
||||
int rows = 5 + r0;
|
||||
// How a message gets there: a chain from you to it, a ring per repeater
|
||||
// on the way.
|
||||
ContactInfo ci;
|
||||
if (hdr + step * 5 + lh <= H && e.contact_idx >= 0 && the_mesh.getContactByIdx(e.contact_idx, ci)) {
|
||||
const int y = hdr + step * 5;
|
||||
if (hdr + step * rows + lh <= H && e.contact_idx >= 0 && the_mesh.getContactByIdx(e.contact_idx, ci)) {
|
||||
const int y = hdr + step * rows;
|
||||
display.setCursor(2, y);
|
||||
display.print("Path:");
|
||||
int x = 2 + display.getTextWidth("Path: ");
|
||||
if (ci.out_path_len == 0xFF) {
|
||||
display.print(" flood");
|
||||
} else {
|
||||
const int hops = ci.out_path_len & 63, shown = hops > 5 ? 5 : hops;
|
||||
const int hops = ci.out_path_len & 63;
|
||||
const int r = lh / 4 > 2 ? lh / 4 : 2, cy = y + lh / 2 - 1, gap = r + 3;
|
||||
// up to 5 rings, as many as leave room for the count after them
|
||||
const int fit = (W - 2 - x - display.getTextWidth("5 hops")) / (2 * r + 1 + gap) - 2;
|
||||
const int cap = fit < 5 ? (fit < 1 ? 1 : fit) : 5, shown = hops > cap ? cap : hops;
|
||||
for (int k = 0; k <= shown + 1; k++) { // you and it filled, the hops as rings
|
||||
if (k) for (int dx = x - gap + 1; dx < x; dx += 2) display.fillRect(dx, cy, 1, 1);
|
||||
info::circle(display, x + r, cy, r);
|
||||
@@ -490,12 +502,12 @@ class NearbyScreen : public UIScreen, protected NearbyModel {
|
||||
display.setCursor(x, y);
|
||||
display.print(buf);
|
||||
}
|
||||
rows = 6;
|
||||
rows++;
|
||||
}
|
||||
|
||||
// Where it is, on a compass rose (north up), where the screen has the room
|
||||
// (e-ink): beside the rows on a wide one, under them on a tall one.
|
||||
if (e.dist_km < 0.0f) return;
|
||||
if (!Features::IS_EINK || e.dist_km < 0.0f) return;
|
||||
const int below = hdr + step * rows + lh;
|
||||
int r, cx, cy;
|
||||
if (H - below > W / 2) { r = (W < H - below ? W : H - below) / 2 - lh; cx = W / 2; cy = below + lh / 2 + r; }
|
||||
@@ -650,8 +662,8 @@ public:
|
||||
}
|
||||
|
||||
int item_h = display.lineStep();
|
||||
// E-ink sets an arrow towards the node before its distance: one more cell.
|
||||
const bool arrows = Features::IS_EINK && _source == SRC_STORED && _sort != SORT_TIME;
|
||||
// An arrow towards the node before its distance: one more cell.
|
||||
const bool arrows = _source == SRC_STORED && _sort != SORT_TIME;
|
||||
int dist_col = display.width() - display.getCharWidth() * (arrows ? 9 : 7);
|
||||
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
|
||||
@@ -48,17 +48,13 @@ private:
|
||||
CayenneLPP _lpp;
|
||||
unsigned long _lpp_at = 0;
|
||||
|
||||
// History lines, one sample a minute (32 min). E-ink draws them as charts
|
||||
// (chartBlock()) over a longer span: a sample every 5 minutes, 8 hours.
|
||||
// History lines (ui-core/StatusHistory.h), one sample a minute (32 min).
|
||||
// E-ink draws them as charts (chartBlock()) over a longer span: a sample
|
||||
// every 5 minutes, 8 hours. Its Radio chart also has the SNR of the last
|
||||
// 32 packets.
|
||||
static const int HIST_N = Features::IS_EINK ? 96 : 32;
|
||||
static const unsigned long SAMPLE_MS = Features::IS_EINK ? 300000 : 60000;
|
||||
static constexpr const char* HIST_SPAN = Features::IS_EINK ? "8 h ago" : "32 min";
|
||||
unsigned long _next_sample = 0;
|
||||
uint32_t _last_rx = 0;
|
||||
info::History<HIST_N> _noise, _batt, _traffic;
|
||||
// The SNR of each packet heard, the last 32 (e-ink's Radio chart), x4 dB.
|
||||
info::History<32> _snr;
|
||||
uint32_t _snr_rx = 0;
|
||||
uicore::StatusHistory<HIST_N, Features::IS_EINK ? 300000UL : 60000UL> _hist;
|
||||
|
||||
// A chart as a block (e-ink): `title` with the value now on its line, the
|
||||
// chart under it.
|
||||
@@ -124,12 +120,12 @@ private:
|
||||
const int nf = (int)radio_driver.getNoiseFloor();
|
||||
if (nf) snprintf(a, sizeof(a), "%d dBm", nf); else strcpy(a, "-");
|
||||
if (Features::IS_EINK) {
|
||||
chartBlock(d, f, rsv, "Noise", a, _noise, 6, false, HIST_SPAN);
|
||||
chartBlock(d, f, rsv, "Noise", a, _hist.noise, 6, false, HIST_SPAN);
|
||||
} else if (f.place(d.lineStep())) { // label, the last half hour, the value now
|
||||
info::valueRow(d, f.at, "Noise", a, false, rsv, 1);
|
||||
const int x0 = 1 + d.getTextWidth("Noise") + 6;
|
||||
const int x1 = d.width() - rsv - 2 - d.getTextWidth(a) - 6;
|
||||
if (x1 - x0 > 12) info::spark(d, x0, f.at, x1 - x0, lh - 1, _noise);
|
||||
if (x1 - x0 > 12) info::spark(d, x0, f.at, x1 - x0, lh - 1, _hist.noise);
|
||||
}
|
||||
if (f.place(d.lineStep())) {
|
||||
const float snr = radio_driver.getLastSNR();
|
||||
@@ -138,7 +134,7 @@ private:
|
||||
drawSignalBars(d, d.width() - rsv - 2 - d.getTextWidth(b) - 5, f.at, (int)(snr * 4));
|
||||
}
|
||||
snprintf(b, sizeof(b), "%.1f dB", radio_driver.getLastSNR());
|
||||
if (Features::IS_EINK) chartBlock(d, f, rsv, "SNR", b, _snr, 20, false, "32 pkts");
|
||||
if (Features::IS_EINK) chartBlock(d, f, rsv, "SNR", b, _hist.snr, 20, false, "32 pkts");
|
||||
else row(d, f, rsv, "Last SNR", b);
|
||||
}
|
||||
|
||||
@@ -214,12 +210,12 @@ private:
|
||||
row(d, f, rsv, "Battery", a);
|
||||
row(d, f, rsv, "Source", board.isExternalPowered() ? "USB" : "Battery");
|
||||
if (Features::IS_EINK) {
|
||||
chartBlock(d, f, rsv, "Trend", nullptr, _batt, 20, false, HIST_SPAN);
|
||||
chartBlock(d, f, rsv, "Trend", nullptr, _hist.batt, 20, false, HIST_SPAN);
|
||||
} else if (f.place(d.lineStep())) {
|
||||
d.setCursor(1, f.at);
|
||||
d.print("Trend");
|
||||
const int x0 = 1 + d.getTextWidth("Trend") + 6;
|
||||
info::spark(d, x0, f.at, d.width() - rsv - 2 - x0, d.getLineHeight() - 1, _batt, 20);
|
||||
info::spark(d, x0, f.at, d.width() - rsv - 2 - x0, d.getLineHeight() - 1, _hist.batt, 20);
|
||||
}
|
||||
|
||||
// Sensors: every reading but the battery (above) and the position (GPS).
|
||||
@@ -259,9 +255,9 @@ private:
|
||||
if (Features::IS_EINK) { // packets per 5 minutes, as bars
|
||||
const int ch = 3 * lh;
|
||||
if (f.place(info::chartH(d, ch) + 2))
|
||||
info::chart(d, 1, f.at, d.width() - rsv - 3, ch, _traffic, 4, true, HIST_SPAN, "now");
|
||||
info::chart(d, 1, f.at, d.width() - rsv - 3, ch, _hist.traffic, 4, true, HIST_SPAN, "now");
|
||||
} else if (f.place(lh + 6)) {
|
||||
info::spark(d, 1, f.at, d.width() - rsv - 3, lh + 3, _traffic, 4);
|
||||
info::spark(d, 1, f.at, d.width() - rsv - 3, lh + 3, _hist.traffic, 4);
|
||||
}
|
||||
|
||||
static const uint8_t MSG[] = { PAYLOAD_TYPE_TXT_MSG, PAYLOAD_TYPE_GRP_TXT };
|
||||
@@ -350,26 +346,12 @@ public:
|
||||
// GPS stays awake while its live tabs are on screen.
|
||||
bool wantsLiveGps() const { return tab() == TAB_GPS || tab() == TAB_SKY; }
|
||||
|
||||
// Called from the UI loop on every pass: one sample a minute for the lines.
|
||||
// Called from the UI loop on every pass; the history keeps its own pace.
|
||||
void sample() {
|
||||
if (Features::IS_EINK) samplePacket();
|
||||
const unsigned long now = millis();
|
||||
if (_next_sample && (long)(now - _next_sample) < 0) return;
|
||||
_next_sample = now + SAMPLE_MS;
|
||||
const int nf = (int)radio_driver.getNoiseFloor();
|
||||
if (nf) _noise.push((int16_t)nf);
|
||||
_batt.push((int16_t)battMv());
|
||||
const uint32_t rx = totalRx();
|
||||
if (_last_rx || rx) _traffic.push((int16_t)(rx - _last_rx > 30000 ? 30000 : rx - _last_rx));
|
||||
_last_rx = rx;
|
||||
}
|
||||
// On every pass: the SNR of a packet just heard (e-ink's Radio chart).
|
||||
void samplePacket() {
|
||||
const uint32_t rx = totalRx();
|
||||
if (rx != _snr_rx) { _snr.push((int16_t)(radio_driver.getLastSNR() * 4)); _snr_rx = rx; }
|
||||
_hist.tick(millis(), (int)radio_driver.getNoiseFloor(), battMv(), totalRx(), radio_driver.getLastSNR());
|
||||
}
|
||||
|
||||
const info::History<HIST_N>& battHistory() const { return _batt; }
|
||||
const info::History<HIST_N>& battHistory() const { return _hist.batt; }
|
||||
|
||||
// Contacts heard from in the last hour (cached for 10 s: it walks them all).
|
||||
static int heardLastHour() {
|
||||
@@ -422,7 +404,7 @@ public:
|
||||
auto res = _reset_menu.handleInput(c);
|
||||
if (res == PopupMenu::SELECTED && _reset_menu.selectedIndex() == 0) {
|
||||
diag::resetCounters();
|
||||
_last_rx = 0;
|
||||
_hist.countersReset();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
#include "InfoKit.h"
|
||||
#include "../Trail.h"
|
||||
#include "../ui-core/TrailProfile.h"
|
||||
#include "../GeoUtils.h"
|
||||
#include "GfxUtils.h"
|
||||
#include "icons.h" // scalable mini-icons (map markers, north arrow, grid dots)
|
||||
@@ -51,7 +52,7 @@ class TrailScreen : public UIScreen {
|
||||
UITask* _task;
|
||||
TrailStore* _store;
|
||||
|
||||
enum View { V_SUMMARY = 0, V_MAP = 1, V_LIST = 2, V_COUNT };
|
||||
enum View { V_SUMMARY = 0, V_MAP = 1, V_PROFILE = 2, V_LIST = 3, V_COUNT };
|
||||
uint8_t _view = V_SUMMARY;
|
||||
int _summary_scroll = 0;
|
||||
int _list_scroll = 0;
|
||||
@@ -126,14 +127,16 @@ public:
|
||||
|
||||
// The title bar carries the view dots so the bottom hint row can be
|
||||
// reclaimed for content.
|
||||
const char* title = (_view == V_MAP) ? (_map_grid ? "Trail map+" : "Trail map")
|
||||
: (_view == V_LIST) ? "Trail list"
|
||||
: "Trail";
|
||||
const char* title = (_view == V_MAP) ? (_map_grid ? "Trail map+" : "Trail map")
|
||||
: (_view == V_PROFILE) ? "Trail height"
|
||||
: (_view == V_LIST) ? "Trail list"
|
||||
: "Trail";
|
||||
drawScreenHeader(display, title, (int)_view, (int)V_COUNT);
|
||||
|
||||
if (_view == V_MAP) renderMap(display);
|
||||
else if (_view == V_LIST) renderList(display);
|
||||
else renderSummary(display);
|
||||
if (_view == V_MAP) renderMap(display);
|
||||
else if (_view == V_PROFILE) renderProfileView(display);
|
||||
else if (_view == V_LIST) renderList(display);
|
||||
else renderSummary(display);
|
||||
|
||||
if (_action_menu.active) _action_menu.render(display);
|
||||
return _store->isActive() ? 1000 : 5000;
|
||||
@@ -558,9 +561,16 @@ private:
|
||||
}
|
||||
}
|
||||
|
||||
bool hasAltitude() const {
|
||||
for (int i = 0; i < _store->count(); i++) if (_store->at(i).alt_m != TRAIL_ALT_NONE) return true;
|
||||
return false;
|
||||
bool hasAltitude() const { return trailprofile::stats(*_store).any; }
|
||||
|
||||
// The profile as its own view, the whole space under the header.
|
||||
void renderProfileView(DisplayDriver& d) {
|
||||
const int top = d.listStart();
|
||||
if (!hasAltitude()) {
|
||||
d.drawTextCentered(d.width() / 2, (top + d.height()) / 2 - d.getLineHeight() / 2, "No altitude yet");
|
||||
return;
|
||||
}
|
||||
renderProfile(d, 1, top, d.width() - 2, d.height() - 1 - top);
|
||||
}
|
||||
|
||||
// The height over the distance walked, in x, y, w, h: a framed area, a
|
||||
@@ -568,30 +578,17 @@ private:
|
||||
// point under it.
|
||||
void renderProfile(DisplayDriver& d, int x, int y, int w, int h) {
|
||||
const int lh = d.getLineHeight(), fh = h - lh - 2; // the frame; the caption under it
|
||||
const int n = _store->count();
|
||||
int lo = 32767, hi = -32768, gain = 0, last = TRAIL_ALT_NONE;
|
||||
for (int i = 0; i < n; i++) {
|
||||
const int a = _store->at(i).alt_m;
|
||||
if (a == TRAIL_ALT_NONE) continue;
|
||||
if (a < lo) lo = a;
|
||||
if (a > hi) hi = a;
|
||||
if (last == TRAIL_ALT_NONE || a < last) last = a; // the climb, past 3 m of jitter
|
||||
else if (a - last >= 3) { gain += a - last; last = a; }
|
||||
}
|
||||
const trailprofile::Stats st = trailprofile::stats(*_store);
|
||||
int lo = st.lo, hi = st.hi;
|
||||
if (hi - lo < 20) { const int mid = (hi + lo) / 2; lo = mid - 10; hi = mid + 10; }
|
||||
const float total = _store->totalDistanceMeters() > 0 ? (float)_store->totalDistanceMeters() : 1.0f;
|
||||
d.drawSoftRect(x, y, w, fh);
|
||||
const int ix = x + 2, iy = y + 2, iw = w - 4, ih = fh - 4;
|
||||
float cum = 0;
|
||||
int px = -1, py = 0;
|
||||
for (int i = 0; i < n; i++) {
|
||||
const TrailPoint& p = _store->at(i);
|
||||
if (i > 0 && !(p.flags & TRAIL_FLAG_SEG_START))
|
||||
cum += TrailStore::haversineMeters(_store->at(i - 1).lat_1e6, _store->at(i - 1).lon_1e6, p.lat_1e6, p.lon_1e6);
|
||||
if (p.alt_m == TRAIL_ALT_NONE) continue;
|
||||
trailprofile::walk(*_store, [&](float cum, int alt) {
|
||||
int cx = ix + (int)(cum / total * (iw - 1));
|
||||
if (cx > ix + iw - 1) cx = ix + iw - 1;
|
||||
const int cy = iy + ih - 1 - (p.alt_m - lo) * (ih - 1) / (hi - lo);
|
||||
const int cy = iy + ih - 1 - (alt - lo) * (ih - 1) / (hi - lo);
|
||||
if (px >= 0) {
|
||||
for (int c = px + 1; c <= cx; c++) { // each column: the line, the fill under it
|
||||
const int ly = py + (cy - py) * (c - px) / (cx - px);
|
||||
@@ -600,10 +597,11 @@ private:
|
||||
}
|
||||
}
|
||||
px = cx; py = cy;
|
||||
}
|
||||
});
|
||||
const int gain = st.gain;
|
||||
char a[16], b[16];
|
||||
snprintf(a, sizeof(a), "+%d m", gain);
|
||||
snprintf(b, sizeof(b), "max %d m", hi);
|
||||
snprintf(b, sizeof(b), "max %d m", st.hi);
|
||||
const int cy = y + fh + 2;
|
||||
miniIconDraw(d, x, cy, ICON_ARROW_NE);
|
||||
d.setCursor(x + ICON_ARROW_NE.w * miniIconScale(d) + 2, cy);
|
||||
|
||||
@@ -1072,20 +1072,9 @@ public:
|
||||
// The days left this month the alarm goes off on, as calendar::draw()'s
|
||||
// marks: the repeat days, or a one-shot's next (today or tomorrow).
|
||||
uint32_t alarmDays(const struct tm& t) const {
|
||||
if (!_node_prefs || !_node_prefs->alarm_on) return 0;
|
||||
const int n = calendar::daysIn(t.tm_year, t.tm_mon);
|
||||
const uint8_t mask = _node_prefs->alarm_repeat_mask;
|
||||
const bool passed = t.tm_hour * 60 + t.tm_min >= _node_prefs->alarm_hour * 60 + _node_prefs->alarm_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;
|
||||
if (!_node_prefs) return 0;
|
||||
return calmath::alarmDays(t, _node_prefs->alarm_on, _node_prefs->alarm_repeat_mask,
|
||||
_node_prefs->alarm_hour, _node_prefs->alarm_min);
|
||||
}
|
||||
|
||||
void drawClockPage(DisplayDriver& d, uint32_t unix_ts) {
|
||||
|
||||
Reference in New Issue
Block a user