mirror of
https://github.com/MarekZegare4/MeshCore-Solo.git
synced 2026-11-02 19:02:00 +00:00
feat: a node map and a calendar on ui-new, a banner switch on the L2
- Nearby › Map (ui-new): the positioned nodes and you on one view, scaled to fit, with north, a scale bar and the selected node's name and distance; Left / Right picks a node, Up / Down zooms on it, Enter opens it. - Clock tools › Calendar (ui-new): the month with today and the alarm's days; a tight grid on small screens, Left / Right pages the months. - L2 Settings: a Message banner switch (on / off) in place of ui-new's three-way alert setting. - Simulator: sim_nodes() puts eight positioned contacts around Kraków. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -13,6 +13,7 @@ for what you can do with it:
|
|||||||
|
|
||||||
| Option | Does |
|
| Option | Does |
|
||||||
| ------ | ---- |
|
| ------ | ---- |
|
||||||
|
| Map | Every node with a position on a map (below) |
|
||||||
| Navigate | Distance and bearing to the node; follows it if it shares its position live |
|
| Navigate | Distance and bearing to the node; follows it if it shares its position live |
|
||||||
| Ping | Sends a ping and shows the round trip time and SNR |
|
| Ping | Sends a ping and shows the round trip time and SNR |
|
||||||
| Save waypoint, Set as target | Its position as a waypoint, or as the [Locator](./navigation.md#locator) target |
|
| Save waypoint, Set as target | Its position as a waypoint, or as the [Locator](./navigation.md#locator) target |
|
||||||
@@ -20,6 +21,13 @@ for what you can do with it:
|
|||||||
| Admin | Remote admin, for a repeater or room (below) |
|
| Admin | Remote admin, for a repeater or room (below) |
|
||||||
| Discover scan | Asks the repeaters, rooms and sensors in direct range to answer, with their signal |
|
| Discover scan | Asks the repeaters, rooms and sensors in direct range to answer, with their signal |
|
||||||
|
|
||||||
|
**Map** shows the nodes of the current filter that have a position, with you
|
||||||
|
and the target, fitted to the screen, north up, with a scale. Nodes are dots
|
||||||
|
(a diamond for a live share); the selected one is boxed, and the line under
|
||||||
|
the map gives its name and distance, with **+n** when others sit on the same
|
||||||
|
spot. **Left / Right** selects the next node, **Up / Down** zooms in on it and
|
||||||
|
back out, **Enter** opens its details and **Hold Enter** its options.
|
||||||
|
|
||||||
> [!NOTE]
|
> [!NOTE]
|
||||||
> **Wio Tracker L2:** the **Nodes** app. Filter chips at the top, the sort in
|
> **Wio Tracker L2:** the **Nodes** app. Filter chips at the top, the sort in
|
||||||
> the header, and a map button that shows every node with a position. A
|
> the header, and a map button that shows every node with a position. A
|
||||||
|
|||||||
@@ -77,6 +77,10 @@ Settings › Display › Message alert picks how a new message shows: **Normal**
|
|||||||
blinking for a moment, covering nothing else) or **Off** (no alert; the sound
|
blinking for a moment, covering nothing else) or **Off** (no alert; the sound
|
||||||
and waking the screen still follow their own settings).
|
and waking the screen still follow their own settings).
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> **Wio Tracker L2:** Settings › Display › **Message banner** turns the banner
|
||||||
|
> on or off; tapping it opens the conversation.
|
||||||
|
|
||||||
The battery sits at the top right, as an icon, a percentage or the voltage
|
The battery sits at the top right, as an icon, a percentage or the voltage
|
||||||
(Settings › Display). A small bolt beside it means external power is
|
(Settings › Display). A small bolt beside it means external power is
|
||||||
connected over USB.
|
connected over USB.
|
||||||
|
|||||||
@@ -28,6 +28,9 @@ Settings.
|
|||||||
- **Timer**: a countdown that rings when it reaches zero, whatever screen
|
- **Timer**: a countdown that rings when it reaches zero, whatever screen
|
||||||
you're on.
|
you're on.
|
||||||
- **Stopwatch**.
|
- **Stopwatch**.
|
||||||
|
- **Calendar**: the month with today marked and the alarm's days
|
||||||
|
underlined; **Left / Right** pages through the months, **Enter** comes
|
||||||
|
back to this one.
|
||||||
|
|
||||||
Any key silences the ringing; it stops by itself after a minute.
|
Any key silences the ringing; it stops by itself after a minute.
|
||||||
|
|
||||||
|
|||||||
@@ -1909,7 +1909,8 @@ void UITask::onMessageArrived(const UiEvent& ev) {
|
|||||||
bool open_here = _screen == SCR_THREAD &&
|
bool open_here = _screen == SCR_THREAD &&
|
||||||
(ev.kind == UIEventType::channelMessage ? _thread_is_channel && ev.idx == _thread_channel
|
(ev.kind == UIEventType::channelMessage ? _thread_is_channel && ev.idx == _thread_channel
|
||||||
: !_thread_is_channel && memcmp(_thread_key, ev.key, 4) == 0);
|
: !_thread_is_channel && memcmp(_thread_key, ev.key, 4) == 0);
|
||||||
if (!open_here) bannerShow(icon, ev.text, text); // not over the very conversation it's in
|
bool banner = !_prefs || _prefs->msg_alert != NodePrefs::MSG_ALERT_OFF;
|
||||||
|
if (!open_here && banner) bannerShow(icon, ev.text, text); // not over the very conversation it's in
|
||||||
if (_screen == SCR_CHATS && !_nav_overlay) refreshChats(); // new unread counts (not under an open popup)
|
if (_screen == SCR_CHATS && !_nav_overlay) refreshChats(); // new unread counts (not under an open popup)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4307,6 +4308,22 @@ static lv_obj_t* switchRow(lv_obj_t* parent, const char* text, const char* sub,
|
|||||||
|
|
||||||
// ── Schema-driven settings (ui-core/SettingsSchema.h) ─────────────────────────
|
// ── Schema-driven settings (ui-core/SettingsSchema.h) ─────────────────────────
|
||||||
|
|
||||||
|
// Settings > Display: ui-new's Message alert as a switch -- the banner, or
|
||||||
|
// none (Normal / Off; the L2 has no corner for Compact's envelope).
|
||||||
|
static void onMsgBannerSwitch(lv_event_t* e) {
|
||||||
|
s_ui->setMsgBanner(lv_obj_has_state((lv_obj_t*)lv_event_get_target(e), LV_STATE_CHECKED));
|
||||||
|
}
|
||||||
|
void UITask::msgBannerRow(lv_obj_t* card) {
|
||||||
|
lv_obj_t* sw = switchRow(card, "Message banner", "A new message drops in from the top", nullptr);
|
||||||
|
if (!_prefs || _prefs->msg_alert != NodePrefs::MSG_ALERT_OFF) lv_obj_add_state(sw, LV_STATE_CHECKED);
|
||||||
|
lv_obj_add_event_cb(sw, onMsgBannerSwitch, LV_EVENT_VALUE_CHANGED, NULL);
|
||||||
|
}
|
||||||
|
void UITask::setMsgBanner(bool on) {
|
||||||
|
if (!_prefs) return;
|
||||||
|
_prefs->msg_alert = on ? NodePrefs::MSG_ALERT_NORMAL : NodePrefs::MSG_ALERT_OFF;
|
||||||
|
prefsSave();
|
||||||
|
}
|
||||||
|
|
||||||
static void onSchemaSwitch(lv_event_t* e) {
|
static void onSchemaSwitch(lv_event_t* e) {
|
||||||
s_ui->setSchemaValue((int)(uintptr_t)lv_event_get_user_data(e),
|
s_ui->setSchemaValue((int)(uintptr_t)lv_event_get_user_data(e),
|
||||||
lv_obj_has_state((lv_obj_t*)lv_event_get_target(e), LV_STATE_CHECKED) ? 1 : 0);
|
lv_obj_has_state((lv_obj_t*)lv_event_get_target(e), LV_STATE_CHECKED) ? 1 : 0);
|
||||||
@@ -4507,7 +4524,7 @@ void UITask::schemaSection(int i) {
|
|||||||
if (sec == settings::SEC_SOUND) buildSoundRows(card, true); // On / Off / Auto
|
if (sec == settings::SEC_SOUND) buildSoundRows(card, true); // On / Off / Auto
|
||||||
for (int k = i; k < settings::COUNT && settings::ALL[k].section == sec; k++) {
|
for (int k = i; k < settings::COUNT && settings::ALL[k].section == sec; k++) {
|
||||||
if (settings::ALL[k].offset == offsetof(NodePrefs, lock_compact)) continue; // ui-new's lock look; the slide card has one
|
if (settings::ALL[k].offset == offsetof(NodePrefs, lock_compact)) continue; // ui-new's lock look; the slide card has one
|
||||||
if (settings::ALL[k].offset == offsetof(NodePrefs, msg_alert)) continue; // ui-new's alert; the L2 has its own
|
if (settings::ALL[k].offset == offsetof(NodePrefs, msg_alert)) { msgBannerRow(card); continue; } // no corner envelope here
|
||||||
schemaRow(card, k);
|
schemaRow(card, k);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -90,6 +90,7 @@ public:
|
|||||||
void setBrightnessPct(uint8_t pct, bool save);
|
void setBrightnessPct(uint8_t pct, bool save);
|
||||||
void applyDisplayPrefs() override;
|
void applyDisplayPrefs() override;
|
||||||
void setSchemaValue(int idx, int v);
|
void setSchemaValue(int idx, int v);
|
||||||
|
void setMsgBanner(bool on); // Settings > Display > Message banner
|
||||||
void showNearby();
|
void showNearby();
|
||||||
void showMap(); // in its current mode
|
void showMap(); // in its current mode
|
||||||
void openMap(bool nav); // the one map; false: from Nodes, with the nodes on it and Back to Nodes
|
void openMap(bool nav); // the one map; false: from Nodes, with the nodes on it and Back to Nodes
|
||||||
@@ -376,6 +377,7 @@ private:
|
|||||||
void schemaExtras(int page);
|
void schemaExtras(int page);
|
||||||
void showMapOptions(uint8_t section);
|
void showMapOptions(uint8_t section);
|
||||||
void schemaRow(lv_obj_t* card, int idx);
|
void schemaRow(lv_obj_t* card, int idx);
|
||||||
|
void msgBannerRow(lv_obj_t* card);
|
||||||
void buildKeyboardPage(lv_obj_t* body);
|
void buildKeyboardPage(lv_obj_t* body);
|
||||||
void buildAboutPage(lv_obj_t* body);
|
void buildAboutPage(lv_obj_t* body);
|
||||||
void buildAdminPick();
|
void buildAdminPick();
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
// The current month as a grid of days, weeks from Monday, today in a filled
|
// The current month as a grid of days, weeks from Monday, today in a filled
|
||||||
// soft box, marked days (the alarm's) underlined. For the room a tall e-ink
|
// soft box, marked days (the alarm's) underlined. For the room a tall e-ink
|
||||||
// screen has under its clock (the Clock page, the lock screen).
|
// screen has under its clock (the Clock page, the lock screen), and Clock
|
||||||
|
// tools > Calendar. `tight`: rows a line apart, for a 64-pixel screen.
|
||||||
|
|
||||||
#include <time.h>
|
#include <time.h>
|
||||||
#include <helpers/ui/DisplayDriver.h>
|
#include <helpers/ui/DisplayDriver.h>
|
||||||
@@ -15,25 +16,30 @@ using calmath::firstCol;
|
|||||||
using calmath::weeks;
|
using calmath::weeks;
|
||||||
|
|
||||||
// Its height: the day names, a rule, then a row a week.
|
// Its height: the day names, a rule, then a row a week.
|
||||||
inline int height(DisplayDriver& d, const struct tm& t) {
|
inline int height(DisplayDriver& d, const struct tm& t, bool tight = false) {
|
||||||
|
if (tight) return (1 + weeks(t)) * d.getLineHeight() + 1;
|
||||||
return (1 + weeks(t)) * (d.getLineHeight() + 2) + 3;
|
return (1 + weeks(t)) * (d.getLineHeight() + 2) + 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
// `marked`: bit (day - 1) set for each day to underline.
|
// `marked`: bit (day - 1) set for each day to underline. `today`: box
|
||||||
inline void draw(DisplayDriver& d, int x, int y, int w, const struct tm& t, uint32_t marked = 0) {
|
// t.tm_mday (off for another month than the current).
|
||||||
|
inline void draw(DisplayDriver& d, int x, int y, int w, const struct tm& t, uint32_t marked = 0,
|
||||||
|
bool tight = false, bool today = true) {
|
||||||
static const char* const NAMES[] = { "Mo", "Tu", "We", "Th", "Fr", "Sa", "Su" };
|
static const char* const NAMES[] = { "Mo", "Tu", "We", "Th", "Fr", "Sa", "Su" };
|
||||||
const int lh = d.getLineHeight(), step = lh + 2, cw = w / 7;
|
static const char* const INITIALS[] = { "M", "T", "W", "T", "F", "S", "S" };
|
||||||
|
const int lh = d.getLineHeight(), step = tight ? lh : lh + 2, cw = w / 7;
|
||||||
const int x0 = x + (w - 7 * cw) / 2;
|
const int x0 = x + (w - 7 * cw) / 2;
|
||||||
d.setColor(DisplayDriver::LIGHT);
|
d.setColor(DisplayDriver::LIGHT);
|
||||||
for (int c = 0; c < 7; c++) d.drawTextCentered(x0 + c * cw + cw / 2, y, NAMES[c]);
|
for (int c = 0; c < 7; c++) d.drawTextCentered(x0 + c * cw + cw / 2, y, (tight ? INITIALS : NAMES)[c]);
|
||||||
info::rule(d, x0, y + lh + 1, 7 * cw);
|
info::rule(d, x0, y + (tight ? lh - 1 : lh + 1), 7 * cw);
|
||||||
const int top = y + step + 3, col0 = firstCol(t), n = daysIn(t.tm_year, t.tm_mon);
|
const int top = y + (tight ? lh + 1 : step + 3), col0 = firstCol(t), n = daysIn(t.tm_year, t.tm_mon);
|
||||||
char buf[3];
|
char buf[3];
|
||||||
for (int day = 1; day <= n; day++) {
|
for (int day = 1; day <= n; day++) {
|
||||||
const int k = col0 + day - 1, cx = x0 + (k % 7) * cw, cy = top + (k / 7) * step;
|
const int k = col0 + day - 1, cx = x0 + (k % 7) * cw, cy = top + (k / 7) * step;
|
||||||
snprintf(buf, sizeof(buf), "%d", day);
|
snprintf(buf, sizeof(buf), "%d", day);
|
||||||
if (day == t.tm_mday) {
|
if (today && day == t.tm_mday) {
|
||||||
d.fillSoftRect(cx + 1, cy - 1, cw - 2, lh + 2);
|
if (tight) d.fillSoftRect(cx, cy - 1, cw, lh);
|
||||||
|
else d.fillSoftRect(cx + 1, cy - 1, cw - 2, lh + 2);
|
||||||
d.setColor(DisplayDriver::DARK);
|
d.setColor(DisplayDriver::DARK);
|
||||||
d.drawTextCentered(cx + cw / 2, cy, buf);
|
d.drawTextCentered(cx + cw / 2, cy, buf);
|
||||||
d.setColor(DisplayDriver::LIGHT);
|
d.setColor(DisplayDriver::LIGHT);
|
||||||
@@ -42,8 +48,8 @@ inline void draw(DisplayDriver& d, int x, int y, int w, const struct tm& t, uint
|
|||||||
}
|
}
|
||||||
if (marked & (1UL << (day - 1))) { // under the number, in the gap below it
|
if (marked & (1UL << (day - 1))) { // under the number, in the gap below it
|
||||||
const int uw = d.getTextWidth(buf);
|
const int uw = d.getTextWidth(buf);
|
||||||
d.setColor(day == t.tm_mday ? DisplayDriver::DARK : DisplayDriver::LIGHT);
|
d.setColor(today && day == t.tm_mday ? DisplayDriver::DARK : DisplayDriver::LIGHT);
|
||||||
d.fillRect(cx + (cw - uw) / 2, cy + lh, uw, 1);
|
d.fillRect(cx + (cw - uw) / 2, cy + (tight ? lh - 2 : lh), uw, 1);
|
||||||
d.setColor(DisplayDriver::LIGHT);
|
d.setColor(DisplayDriver::LIGHT);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
// Clock tools — Alarm, Countdown timer (minutnik) and Stopwatch (stoper).
|
// Clock tools — Alarm, Countdown timer (minutnik), Stopwatch (stoper) and
|
||||||
// Entered with Enter on the home CLOCK page. A small top menu picks one of the
|
// Calendar. Entered with Enter on the home CLOCK page. A small top menu picks
|
||||||
// three tools; Cancel backs out a level (tool → menu → home).
|
// one of the tools; Cancel backs out a level (tool → menu → home).
|
||||||
//
|
//
|
||||||
// This screen is pure UI. The time-critical machinery lives in the UI Core's
|
// This screen is pure UI. The time-critical machinery lives in the UI Core's
|
||||||
// ClockEngine (ui-core/ClockEngine.h), which UITask drives every loop
|
// ClockEngine (ui-core/ClockEngine.h), which UITask drives every loop
|
||||||
@@ -36,10 +36,12 @@ class ClockToolsScreen : public UIScreen {
|
|||||||
UITask* _task;
|
UITask* _task;
|
||||||
NodePrefs* _prefs;
|
NodePrefs* _prefs;
|
||||||
|
|
||||||
enum View : uint8_t { V_MENU, V_ALARM, V_TIMER, V_STOPWATCH };
|
enum View : uint8_t { V_MENU, V_ALARM, V_TIMER, V_STOPWATCH, V_CALENDAR };
|
||||||
|
static const int MENU_N = 4;
|
||||||
uint8_t _view = V_MENU;
|
uint8_t _view = V_MENU;
|
||||||
int _sel = 0, _scroll = 0; // shared list cursor
|
int _sel = 0, _scroll = 0; // shared list cursor
|
||||||
bool _alarm_dirty = false; // unsaved edits to alarm_* (persist on exit)
|
bool _alarm_dirty = false; // unsaved edits to alarm_* (persist on exit)
|
||||||
|
int _cal_month = 0; // Calendar: months from the current one
|
||||||
|
|
||||||
// Countdown config (the duration to start; the running countdown lives in UITask).
|
// Countdown config (the duration to start; the running countdown lives in UITask).
|
||||||
uint8_t _timer_h = 0, _timer_m = 5, _timer_s = 0;
|
uint8_t _timer_h = 0, _timer_m = 5, _timer_s = 0;
|
||||||
@@ -147,9 +149,9 @@ class ClockToolsScreen : public UIScreen {
|
|||||||
// ── Sub-renders ───────────────────────────────────────────────────────────
|
// ── Sub-renders ───────────────────────────────────────────────────────────
|
||||||
int renderMenu(DisplayDriver& d) {
|
int renderMenu(DisplayDriver& d) {
|
||||||
drawScreenHeader(d, "Clock tools");
|
drawScreenHeader(d, "Clock tools");
|
||||||
static const char* items[3] = { "Alarm", "Timer", "Stopwatch" };
|
static const char* items[MENU_N] = { "Alarm", "Timer", "Stopwatch", "Calendar" };
|
||||||
if (_sel > 2) _sel = 2;
|
if (_sel > MENU_N - 1) _sel = MENU_N - 1;
|
||||||
drawList(d, 3, _sel, _scroll, [&](int i, int y, bool sel, int reserve) {
|
drawList(d, MENU_N, _sel, _scroll, [&](int i, int y, bool sel, int reserve) {
|
||||||
drawRowSelection(d, y, sel, reserve);
|
drawRowSelection(d, y, sel, reserve);
|
||||||
d.setCursor(4, y);
|
d.setCursor(4, y);
|
||||||
d.print(items[i]);
|
d.print(items[i]);
|
||||||
@@ -256,14 +258,60 @@ class ClockToolsScreen : public UIScreen {
|
|||||||
return _sw_running ? liveTickMs(d, 100) : 60000;
|
return _sw_running ? liveTickMs(d, 100) : 60000;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A month `_cal_month` months from now: the alarm's days and today only on
|
||||||
|
// the current one. Where the grid and a title line don't fit (a 64-pixel
|
||||||
|
// screen), the grid goes tight and the month and year stand beside it.
|
||||||
|
int renderCalendar(DisplayDriver& d) {
|
||||||
|
struct tm t;
|
||||||
|
if (!localTm(rtc_clock.getCurrentTime(), _prefs ? _prefs->tz_offset_hours : 0, t) || t.tm_year < 120) {
|
||||||
|
drawScreenHeader(d, "Calendar");
|
||||||
|
d.drawTextCentered(d.width() / 2, d.height() / 2 - d.getLineHeight() / 2, "Clock not set");
|
||||||
|
return 60000;
|
||||||
|
}
|
||||||
|
const bool now = _cal_month == 0;
|
||||||
|
uint32_t marked = now && _prefs ? calmath::alarmDays(t, _prefs->alarm_on, _prefs->alarm_repeat_mask,
|
||||||
|
_prefs->alarm_hour, _prefs->alarm_min) : 0;
|
||||||
|
// The 1st of the month shown, its weekday carried over from today's.
|
||||||
|
int wday1 = ((t.tm_wday - (t.tm_mday - 1)) % 7 + 7) % 7;
|
||||||
|
for (int k = 0; k < _cal_month; k++) {
|
||||||
|
wday1 = (wday1 + calmath::daysIn(t.tm_year, t.tm_mon)) % 7;
|
||||||
|
if (++t.tm_mon == 12) { t.tm_mon = 0; t.tm_year++; }
|
||||||
|
}
|
||||||
|
for (int k = 0; k > _cal_month; k--) {
|
||||||
|
if (--t.tm_mon < 0) { t.tm_mon = 11; t.tm_year--; }
|
||||||
|
wday1 = ((wday1 - calmath::daysIn(t.tm_year, t.tm_mon)) % 7 + 7) % 7;
|
||||||
|
}
|
||||||
|
if (!now) { t.tm_mday = 1; t.tm_wday = wday1; }
|
||||||
|
static const char* const MON[] = { "Jan", "Feb", "Mar", "Apr", "May", "Jun",
|
||||||
|
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec" };
|
||||||
|
char title[16];
|
||||||
|
snprintf(title, sizeof(title), "%s %d", MON[t.tm_mon], t.tm_year + 1900);
|
||||||
|
const int W = d.width(), lh = d.getLineHeight(), cw = d.getCharWidth();
|
||||||
|
const int top = d.listStart();
|
||||||
|
if (calendar::height(d, t) <= d.height() - top) {
|
||||||
|
drawScreenHeader(d, title);
|
||||||
|
const int gw = W < 28 * cw ? W : 28 * cw; // as on the Clock page
|
||||||
|
calendar::draw(d, (W - gw) / 2, top, gw, t, marked, false, now);
|
||||||
|
} else {
|
||||||
|
const int side = 4 * cw + 2; // "2026"
|
||||||
|
calendar::draw(d, 0, 0, W - side, t, marked, true, now);
|
||||||
|
char yr[6];
|
||||||
|
snprintf(yr, sizeof(yr), "%d", t.tm_year + 1900);
|
||||||
|
d.drawTextCentered(W - side / 2, d.height() / 2 - lh, MON[t.tm_mon]);
|
||||||
|
d.drawTextCentered(W - side / 2, d.height() / 2, yr);
|
||||||
|
}
|
||||||
|
return 60000;
|
||||||
|
}
|
||||||
|
|
||||||
// ── Input per view ────────────────────────────────────────────────────────
|
// ── Input per view ────────────────────────────────────────────────────────
|
||||||
bool inputMenu(char c) {
|
bool inputMenu(char c) {
|
||||||
if (c == KEY_CANCEL) { _task->gotoHomeScreen(); return true; }
|
if (c == KEY_CANCEL) { _task->gotoHomeScreen(); return true; }
|
||||||
if (c == KEY_UP) { _sel = (_sel > 0) ? _sel - 1 : 2; return true; }
|
if (c == KEY_UP) { _sel = (_sel > 0) ? _sel - 1 : MENU_N - 1; return true; }
|
||||||
if (c == KEY_DOWN) { _sel = (_sel < 2) ? _sel + 1 : 0; return true; }
|
if (c == KEY_DOWN) { _sel = (_sel < MENU_N - 1) ? _sel + 1 : 0; return true; }
|
||||||
if (c == KEY_ENTER) {
|
if (c == KEY_ENTER) {
|
||||||
_view = (_sel == 0) ? V_ALARM : (_sel == 1) ? V_TIMER : V_STOPWATCH;
|
static const View V[MENU_N] = { V_ALARM, V_TIMER, V_STOPWATCH, V_CALENDAR };
|
||||||
_sel = 0; _scroll = 0;
|
_view = V[_sel];
|
||||||
|
_sel = 0; _scroll = 0; _cal_month = 0;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
@@ -348,6 +396,15 @@ class ClockToolsScreen : public UIScreen {
|
|||||||
return true; // other keys just refresh the readout
|
return true; // other keys just refresh the readout
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Calendar: Left / Right (Up / Down too) a month, Enter back to this one.
|
||||||
|
bool inputCalendar(char c) {
|
||||||
|
if (c == KEY_CANCEL) { _view = V_MENU; _sel = 3; return true; }
|
||||||
|
if (keyIsPrev(c) || c == KEY_UP) { if (_cal_month > -120) _cal_month--; return true; }
|
||||||
|
if (keyIsNext(c) || c == KEY_DOWN) { if (_cal_month < 120) _cal_month++; return true; }
|
||||||
|
if (c == KEY_ENTER) { _cal_month = 0; return true; }
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
public:
|
public:
|
||||||
ClockToolsScreen(UITask* task, NodePrefs* prefs) : _task(task), _prefs(prefs) {}
|
ClockToolsScreen(UITask* task, NodePrefs* prefs) : _task(task), _prefs(prefs) {}
|
||||||
|
|
||||||
@@ -364,6 +421,7 @@ public:
|
|||||||
case V_ALARM: return renderAlarm(display);
|
case V_ALARM: return renderAlarm(display);
|
||||||
case V_TIMER: return renderTimer(display);
|
case V_TIMER: return renderTimer(display);
|
||||||
case V_STOPWATCH: return renderStopwatch(display);
|
case V_STOPWATCH: return renderStopwatch(display);
|
||||||
|
case V_CALENDAR: return renderCalendar(display);
|
||||||
default: return renderMenu(display);
|
default: return renderMenu(display);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -373,6 +431,7 @@ public:
|
|||||||
case V_ALARM: return inputAlarm(c);
|
case V_ALARM: return inputAlarm(c);
|
||||||
case V_TIMER: return inputTimer(c);
|
case V_TIMER: return inputTimer(c);
|
||||||
case V_STOPWATCH: return inputStopwatch(c);
|
case V_STOPWATCH: return inputStopwatch(c);
|
||||||
|
case V_CALENDAR: return inputCalendar(c);
|
||||||
default: return inputMenu(c);
|
default: return inputMenu(c);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ class NearbyScreen : public UIScreen, protected NearbyModel {
|
|||||||
|
|
||||||
// ── action-menu actions (matched by id, not by row index) ────────────────────
|
// ── action-menu actions (matched by id, not by row index) ────────────────────
|
||||||
enum Action : uint8_t { ACT_NAV, ACT_PING, ACT_WAYPOINT, ACT_LOCATOR,
|
enum Action : uint8_t { ACT_NAV, ACT_PING, ACT_WAYPOINT, ACT_LOCATOR,
|
||||||
ACT_ADD, ACT_DELETE, ACT_FAV, ACT_PIN, ACT_ADMIN, ACT_SORT, ACT_SCAN };
|
ACT_ADD, ACT_DELETE, ACT_FAV, ACT_PIN, ACT_ADMIN, ACT_SORT, ACT_SCAN, ACT_MAP };
|
||||||
|
|
||||||
// Set by UITask::pickAdminTarget() (Tools > Admin, which is remote-only):
|
// Set by UITask::pickAdminTarget() (Tools > Admin, which is remote-only):
|
||||||
// while true, ENTER on an eligible row (a stored repeater/room contact) hands
|
// while true, ENTER on an eligible row (a stored repeater/room contact) hands
|
||||||
@@ -36,6 +36,8 @@ class NearbyScreen : public UIScreen, protected NearbyModel {
|
|||||||
int _scroll;
|
int _scroll;
|
||||||
bool _detail;
|
bool _detail;
|
||||||
bool _nav = false; // full-screen navigate-to-node view (over detail)
|
bool _nav = false; // full-screen navigate-to-node view (over detail)
|
||||||
|
bool _map = false; // the nodes with a position on a map (under detail)
|
||||||
|
int _map_zoom = 0; // 0: all of them in view; each step halves it, on the selected
|
||||||
navview::EtaTracker _nav_eta; // closing-speed/ETA for the navigate view
|
navview::EtaTracker _nav_eta; // closing-speed/ETA for the navigate view
|
||||||
unsigned long _detail_refresh_ms;
|
unsigned long _detail_refresh_ms;
|
||||||
unsigned long _list_refresh_ms = 0;
|
unsigned long _list_refresh_ms = 0;
|
||||||
@@ -53,7 +55,7 @@ class NearbyScreen : public UIScreen, protected NearbyModel {
|
|||||||
PopupMenu _ping_menu; // ping (special: read-only result rows)
|
PopupMenu _ping_menu; // ping (special: read-only result rows)
|
||||||
PopupMenu _confirm; // delete-contact confirmation (destructive → 2-step)
|
PopupMenu _confirm; // delete-contact confirmation (destructive → 2-step)
|
||||||
|
|
||||||
Action _menu_actions[10]; // parallel to _menu rows — stable action ids
|
Action _menu_actions[12]; // parallel to _menu rows — stable action ids
|
||||||
int _menu_action_count;
|
int _menu_action_count;
|
||||||
char _sort_label[16]; // dynamic label for the Sort row
|
char _sort_label[16]; // dynamic label for the Sort row
|
||||||
|
|
||||||
@@ -327,6 +329,7 @@ class NearbyScreen : public UIScreen, protected NearbyModel {
|
|||||||
_menu_actions[_menu_action_count++] = a;
|
_menu_actions[_menu_action_count++] = a;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (stored && !_map && positioned(0, 1) >= 0) add("Map", ACT_MAP);
|
||||||
if (has_gps) add("Navigate", ACT_NAV);
|
if (has_gps) add("Navigate", ACT_NAV);
|
||||||
if (has_key) add("Ping", ACT_PING);
|
if (has_key) add("Ping", ACT_PING);
|
||||||
if (has_gps) add("Save waypoint", ACT_WAYPOINT);
|
if (has_gps) add("Save waypoint", ACT_WAYPOINT);
|
||||||
@@ -428,6 +431,13 @@ class NearbyScreen : public UIScreen, protected NearbyModel {
|
|||||||
}
|
}
|
||||||
case ACT_SORT: break; // value rows -- see cycleMenuValue()
|
case ACT_SORT: break; // value rows -- see cycleMenuValue()
|
||||||
case ACT_SCAN: enterScan(); break;
|
case ACT_SCAN: enterScan(); break;
|
||||||
|
case ACT_MAP: {
|
||||||
|
_map = true; _map_zoom = 0;
|
||||||
|
_detail_refresh_ms = millis();
|
||||||
|
const Entry* e = selected();
|
||||||
|
if (!e || (e->lat_e6 == 0 && e->lon_e6 == 0)) { int k = positioned(_sel, 1); if (k >= 0) _sel = k; }
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -523,6 +533,99 @@ class NearbyScreen : public UIScreen, protected NearbyModel {
|
|||||||
info::rose(display, cx, cy, r, geo::bearingDeg(_own_lat, _own_lon, e.lat_e6, e.lon_e6));
|
info::rose(display, cx, cy, r, geo::bearingDeg(_own_lat, _own_lon, e.lat_e6, e.lon_e6));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── map view ────────────────────────────────────────────────────────────────
|
||||||
|
static bool hasPos(const Entry& e) { return e.lat_e6 != 0 || e.lon_e6 != 0; }
|
||||||
|
// The next entry with a position from `from` on in direction `dir` (from
|
||||||
|
// itself included), wrapping; -1 when none has one.
|
||||||
|
int positioned(int from, int dir) const {
|
||||||
|
for (int k = 0; k < _count; k++) {
|
||||||
|
int i = ((from + dir * k) % _count + _count) % _count;
|
||||||
|
if (hasPos(_entries[i])) return i;
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Every node with a position (and you) fitted in; zoomed in, centred on the
|
||||||
|
// selected one. Dots, the selected one boxed, live shares as diamonds;
|
||||||
|
// under the map its name and distance, "+n" for others under the same spot.
|
||||||
|
void renderMap(DisplayDriver& d) {
|
||||||
|
const int W = d.width(), H = d.height(), lh = d.getLineHeight(), s = miniIconScale(d);
|
||||||
|
const int bar_y = H - lh - 1, ax = 1, ay = 1, aw = W - 2, ah = bar_y - 3;
|
||||||
|
d.setColor(DisplayDriver::LIGHT);
|
||||||
|
int32_t tla = 0, tlo = 0;
|
||||||
|
const bool target = _task->activeTargetPos(tla, tlo);
|
||||||
|
int32_t mnla = 0, mxla = 0, mnlo = 0, mxlo = 0;
|
||||||
|
bool init = false;
|
||||||
|
auto fold = [&](int32_t la, int32_t lo) {
|
||||||
|
if (!init) { mnla = mxla = la; mnlo = mxlo = lo; init = true; return; }
|
||||||
|
if (la < mnla) mnla = la; if (la > mxla) mxla = la;
|
||||||
|
if (lo < mnlo) mnlo = lo; if (lo > mxlo) mxlo = lo;
|
||||||
|
};
|
||||||
|
for (int i = 0; i < _count; i++) if (hasPos(_entries[i])) fold(_entries[i].lat_e6, _entries[i].lon_e6);
|
||||||
|
if (_own_gps) fold(_own_lat, _own_lon);
|
||||||
|
if (!init || _sel >= _count || !hasPos(_entries[_sel])) {
|
||||||
|
d.drawTextCentered(W / 2, H / 2 - lh / 2, "No node positions");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const Entry& sel = _entries[_sel];
|
||||||
|
float kx = cosf(((mnla + mxla) / 2.0e6f) * (float)M_PI / 180.0f);
|
||||||
|
if (kx < 0.05f) kx = 0.05f;
|
||||||
|
float span_la = (float)(mxla - mnla), span_lo = (float)(mxlo - mnlo) * kx;
|
||||||
|
if (span_la < 2000.0f) span_la = 2000.0f; // ~200 m: one node alone still has a scale
|
||||||
|
if (span_lo < 2000.0f) span_lo = 2000.0f;
|
||||||
|
// Fitted inside a margin clear of the north mark and the scale's label.
|
||||||
|
const int mx = ICON_MAP_NORTH.w * s + 2, my = lh + 2;
|
||||||
|
const float fw = (float)(aw - 2 * mx), fh = (float)(ah - 2 * my);
|
||||||
|
float scale = fh / span_la < fw / span_lo ? fh / span_la : fw / span_lo;
|
||||||
|
float cla = (mnla + mxla) / 2.0f, clo = (mnlo + mxlo) / 2.0f;
|
||||||
|
if (_map_zoom > 0) { scale *= (float)(1 << _map_zoom); cla = sel.lat_e6; clo = sel.lon_e6; }
|
||||||
|
auto project = [&](int32_t la, int32_t lo, int& x, int& y) {
|
||||||
|
x = ax + aw / 2 + (int)(((float)lo - clo) * kx * scale);
|
||||||
|
y = ay + ah / 2 - (int)(((float)la - cla) * scale);
|
||||||
|
};
|
||||||
|
auto inside = [&](int x, int y) { return x >= ax && x < ax + aw && y >= ay && y < ay + ah; };
|
||||||
|
|
||||||
|
int sx, sy, near = 0;
|
||||||
|
project(sel.lat_e6, sel.lon_e6, sx, sy);
|
||||||
|
for (int i = 0; i < _count; i++) {
|
||||||
|
if (!hasPos(_entries[i])) continue;
|
||||||
|
int x, y; project(_entries[i].lat_e6, _entries[i].lon_e6, x, y);
|
||||||
|
if (i != _sel && abs(x - sx) <= 2 * s && abs(y - sy) <= 2 * s) near++;
|
||||||
|
if (!inside(x, y)) continue;
|
||||||
|
miniIconDrawCentered(d, x, y, _entries[i].is_live ? ICON_MAP_CONTACT : ICON_MAP_DOT);
|
||||||
|
}
|
||||||
|
if (_own_gps) { int x, y; project(_own_lat, _own_lon, x, y); if (inside(x, y)) miniIconDrawCentered(d, x, y, ICON_MAP_CURRENT); }
|
||||||
|
if (target) { int x, y; project(tla, tlo, x, y); if (inside(x, y)) miniIconDrawCentered(d, x, y, ICON_MAP_TARGET); }
|
||||||
|
const int r = 4 * s;
|
||||||
|
d.drawRect(sx - r, sy - r, 2 * r + 1, 2 * r + 1);
|
||||||
|
miniIconDrawTop(d, ax + aw - ICON_MAP_NORTH.w * s - 1, ay, ICON_MAP_NORTH);
|
||||||
|
|
||||||
|
// The scale: a round distance about a quarter of the width, bottom right.
|
||||||
|
const float m_px = 0.111f / scale; // 1e-6 degree of latitude is 0.111 m
|
||||||
|
static const float STEPS[] = { 10, 20, 50, 100, 200, 500, 1000, 2000, 5000, 10000, 20000, 50000, 100000, 200000 };
|
||||||
|
float m = STEPS[0];
|
||||||
|
for (float st : STEPS) { m = st; if (st / m_px >= aw / 5) break; }
|
||||||
|
const int len = (int)(m / m_px);
|
||||||
|
char sc[12];
|
||||||
|
geo::fmtDist(sc, sizeof(sc), m / 1000.0f, useImperial());
|
||||||
|
if (len > 2 && len < aw) {
|
||||||
|
const int ly = ay + ah - 2, lx = ax + aw - len - 1;
|
||||||
|
d.fillRect(lx, ly, len, s);
|
||||||
|
d.setCursor(lx + len - d.getTextWidth(sc), ly - lh);
|
||||||
|
d.print(sc);
|
||||||
|
}
|
||||||
|
|
||||||
|
d.fillRect(0, bar_y - 1, W, d.sepH());
|
||||||
|
char right[16] = "";
|
||||||
|
if (sel.dist_km >= 0.0f) geo::fmtDist(right, sizeof(right), sel.dist_km, useImperial(), true);
|
||||||
|
char name[40];
|
||||||
|
if (near > 0) snprintf(name, sizeof(name), "%s +%d", sel.name, near);
|
||||||
|
else snprintf(name, sizeof(name), "%s", sel.name);
|
||||||
|
const int rw = right[0] ? d.getTextWidth(right) + d.getCharWidth() : 0;
|
||||||
|
d.drawTextEllipsized(1, bar_y + 1, W - 2 - rw, name);
|
||||||
|
if (right[0]) { d.setCursor(W - 1 - d.getTextWidth(right), bar_y + 1); d.print(right); }
|
||||||
|
}
|
||||||
|
|
||||||
void renderScanDetail(DisplayDriver& display) {
|
void renderScanDetail(DisplayDriver& display) {
|
||||||
const Entry& e = _entries[_sel];
|
const Entry& e = _entries[_sel];
|
||||||
const int hdr = display.listStart(); // content top (gap below the header separator)
|
const int hdr = display.listStart(); // content top (gap below the header separator)
|
||||||
@@ -604,6 +707,7 @@ public:
|
|||||||
_sel = _scroll = 0;
|
_sel = _scroll = 0;
|
||||||
_detail = false;
|
_detail = false;
|
||||||
_nav = false;
|
_nav = false;
|
||||||
|
_map = false;
|
||||||
_source = SRC_STORED;
|
_source = SRC_STORED;
|
||||||
// _filter / _sort persist across enter() — set once in the constructor
|
// _filter / _sort persist across enter() — set once in the constructor
|
||||||
_scanning = false;
|
_scanning = false;
|
||||||
@@ -655,6 +759,16 @@ public:
|
|||||||
return _ping_menu.active ? 50 : 2000;
|
return _ping_menu.active ? 50 : 2000;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── map view ─────────────────────────────────────────────────────────────
|
||||||
|
if (_map) {
|
||||||
|
if (millis() - _detail_refresh_ms >= DETAIL_REFRESH_MS) {
|
||||||
|
refreshKeepingSelection();
|
||||||
|
_detail_refresh_ms = millis();
|
||||||
|
}
|
||||||
|
renderMap(display);
|
||||||
|
return renderActivePopup(display) ? 50 : 2000;
|
||||||
|
}
|
||||||
|
|
||||||
// ── list view ────────────────────────────────────────────────────────────
|
// ── list view ────────────────────────────────────────────────────────────
|
||||||
if (_source == SRC_SCAN) {
|
if (_source == SRC_SCAN) {
|
||||||
refreshScan();
|
refreshScan();
|
||||||
@@ -823,6 +937,19 @@ public:
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── map view: Left / Right a node, Up / Down zoom, Enter its detail ──────
|
||||||
|
if (_map) {
|
||||||
|
if (c == KEY_CANCEL) { _map = false; return true; }
|
||||||
|
if (c == KEY_CONTEXT_MENU) { openActionMenu(); return true; }
|
||||||
|
if (_count == 0) return true;
|
||||||
|
if (keyIsPrev(c)) { int k = positioned(_sel - 1, -1); if (k >= 0) _sel = k; return true; }
|
||||||
|
if (keyIsNext(c)) { int k = positioned(_sel + 1, 1); if (k >= 0) _sel = k; return true; }
|
||||||
|
if (c == KEY_UP) { if (_map_zoom < 8) _map_zoom++; return true; }
|
||||||
|
if (c == KEY_DOWN) { if (_map_zoom > 0) _map_zoom--; return true; }
|
||||||
|
if (c == KEY_ENTER) { _detail = true; _detail_refresh_ms = millis(); return true; }
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
// ── list view ───────────────────────────────────────────────────────────
|
// ── list view ───────────────────────────────────────────────────────────
|
||||||
if (c == KEY_CANCEL) {
|
if (c == KEY_CANCEL) {
|
||||||
if (_pick_admin_target) { _pick_admin_target = false; _task->leaveTool(); return true; }
|
if (_pick_admin_target) { _pick_admin_target = false; _task->leaveTool(); return true; }
|
||||||
|
|||||||
@@ -196,8 +196,8 @@ static const telemetry::Style L1_INFO = { "\xc2\xb0", true, false, 5 };
|
|||||||
#include "GpioScreen.h"
|
#include "GpioScreen.h"
|
||||||
#endif
|
#endif
|
||||||
#include "ToolsScreen.h"
|
#include "ToolsScreen.h"
|
||||||
#include "ClockToolsScreen.h" // Alarm / Timer / Stopwatch (Clock page › Enter)
|
#include "CalendarView.h" // the month under a tall clock, Clock tools › Calendar
|
||||||
#include "CalendarView.h" // the month under a tall clock
|
#include "ClockToolsScreen.h" // Alarm / Timer / Stopwatch / Calendar (Clock page › Enter)
|
||||||
|
|
||||||
#include "../ui-core/Battery.h"
|
#include "../ui-core/Battery.h"
|
||||||
|
|
||||||
@@ -2221,6 +2221,29 @@ void UITask::simUnread(int which) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sim tests: a GPS fix in Kraków and eight nodes around it, then Nodes.
|
||||||
|
void UITask::simNodes() {
|
||||||
|
sim_location_provider().set(50.0614f, 19.9372f);
|
||||||
|
static const struct { const char* name; float dla, dlo; uint8_t type; } N[] = {
|
||||||
|
{ "Kasia-T1", 0.012f, 0.020f, ADV_TYPE_CHAT }, { "Wawel rpt", -0.006f, -0.004f, ADV_TYPE_REPEATER },
|
||||||
|
{ "Kopiec", 0.004f, -0.045f, ADV_TYPE_REPEATER }, { "Tomek", 0.0005f, 0.0006f, ADV_TYPE_CHAT },
|
||||||
|
{ "Nowa Huta", 0.020f, 0.110f, ADV_TYPE_REPEATER }, { "Room Rynek", 0.0008f, 0.0004f, ADV_TYPE_ROOM },
|
||||||
|
{ "Ola", -0.030f, 0.015f, ADV_TYPE_CHAT }, { "Bielany", 0.000f, -0.080f, ADV_TYPE_REPEATER },
|
||||||
|
};
|
||||||
|
const uint32_t now = rtc_clock.getCurrentTime();
|
||||||
|
for (int i = 0; i < (int)(sizeof(N) / sizeof(N[0])); i++) {
|
||||||
|
uint8_t key[PUB_KEY_SIZE];
|
||||||
|
for (int k = 0; k < PUB_KEY_SIZE; k++) key[k] = (uint8_t)(0x5A ^ (i * 37 + k * 11));
|
||||||
|
the_mesh.addDiscoveredContact(key, N[i].name, N[i].type);
|
||||||
|
if (ContactInfo* c = the_mesh.lookupContactByPubKey(key, PUB_KEY_SIZE)) {
|
||||||
|
c->gps_lat = (int32_t)((50.0614f + N[i].dla) * 1e6f);
|
||||||
|
c->gps_lon = (int32_t)((19.9372f + N[i].dlo) * 1e6f);
|
||||||
|
c->lastmod = now - i * 600;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setCurrScreen(nearby_screen);
|
||||||
|
}
|
||||||
|
|
||||||
void UITask::simDisplay(bool on) {
|
void UITask::simDisplay(bool on) {
|
||||||
if (!_display) return;
|
if (!_display) return;
|
||||||
if (on) _display->turnOn(); else _display->turnOff();
|
if (on) _display->turnOn(); else _display->turnOff();
|
||||||
@@ -2742,6 +2765,10 @@ extern "C" EMSCRIPTEN_KEEPALIVE void sim_unread(int which) {
|
|||||||
if (g_sim_ui_task_for_js) g_sim_ui_task_for_js->simUnread(which);
|
if (g_sim_ui_task_for_js) g_sim_ui_task_for_js->simUnread(which);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extern "C" EMSCRIPTEN_KEEPALIVE void sim_nodes() {
|
||||||
|
if (g_sim_ui_task_for_js) g_sim_ui_task_for_js->simNodes();
|
||||||
|
}
|
||||||
|
|
||||||
// Sim tests: the display off (0) or on (1), as the auto-off and a key press do.
|
// Sim tests: the display off (0) or on (1), as the auto-off and a key press do.
|
||||||
extern "C" EMSCRIPTEN_KEEPALIVE void sim_display(int on) {
|
extern "C" EMSCRIPTEN_KEEPALIVE void sim_display(int on) {
|
||||||
if (g_sim_ui_task_for_js) g_sim_ui_task_for_js->simDisplay(on != 0);
|
if (g_sim_ui_task_for_js) g_sim_ui_task_for_js->simDisplay(on != 0);
|
||||||
|
|||||||
@@ -288,6 +288,7 @@ public:
|
|||||||
void simMessage(const char* text); // sim tests: a channel message arrives
|
void simMessage(const char* text); // sim tests: a channel message arrives
|
||||||
void simUnread(int which); // sim tests: read and new messages, opened
|
void simUnread(int which); // sim tests: read and new messages, opened
|
||||||
void simDisplay(bool on); // sim tests: the display off / on
|
void simDisplay(bool on); // sim tests: the display off / on
|
||||||
|
void simNodes(); // sim tests: you and nodes with positions, Nodes open
|
||||||
#endif
|
#endif
|
||||||
void openContactDM(const ContactInfo& ci);
|
void openContactDM(const ContactInfo& ci);
|
||||||
void openChannelHistory(uint8_t channel_idx); // Favourites dial: open a pinned channel
|
void openChannelHistory(uint8_t channel_idx); // Favourites dial: open a pinned channel
|
||||||
|
|||||||
Reference in New Issue
Block a user